便携式心电(ECG)与血氧(SpO2)监测仪设计
项目开发背景
随着社会老龄化的加剧和生活节奏的加快,心血管疾病与呼吸系统疾病的发病率持续上升,已成为威胁人类健康的主要因素。心电信号和血氧饱和度是评估心脏功能与呼吸循环状态的两项关键生理参数,对其进行实时、便捷的监测,对于疾病的早期发现、日常健康管理以及慢性病患者的长期监护具有极其重要的意义。
传统的医疗监测设备通常体积庞大、操作复杂且成本高昂,主要局限于医院等临床环境使用,难以满足人们在家庭、户外或移动场景下对健康指标进行日常检查与跟踪的需求。市场现有的部分便携式设备往往功能单一,要么只能测量心电,要么只能测量血氧,无法提供综合性的生理信息参考,且在数据记录的连续性和准确性上仍有提升空间。
近年来,微电子技术与传感技术的飞速发展为高性能、低功耗的便携式医疗设备创造了条件。高集成度的模拟前端芯片与光学传感器使得精密生物电信号和光容积信号的采集处理得以在小尺寸模块上实现;高性能低功耗微控制器为复杂算法的实时运行提供了可能;小型化的显示与存储模块则确保了良好的人机交互与数据管理功能。这些技术进步共同推动着个人健康监护设备向多功能、智能化、可穿戴化方向发展。
在此背景下,开发一款能够同时进行心电与血氧监测的便携式一体化设备显得尤为迫切。本项目旨在设计并实现一种集成三导联心电采集与指夹式血氧检测功能的监护仪,它不仅能实时计算并显示心率和血氧饱和度,绘制直观的波形图,还能存储详细数据以供分析,且具备便携易用、续航时间长的特点。该设备的实现,有望为个人健康管理、社区医疗筛查及远程医疗监护提供一种有效的技术工具,对提升公众健康管理水平、缓解医疗资源压力具有积极意义。
设计实现的功能
(1)通过三导联电极采集人体心电信号(ECG),经放大滤波后计算实时心率并检测心率失常(如漏搏)。
(2)通过指夹式光电传感器采集光电容积脉搏波(PPG),计算血氧饱和度(SpO2)和脉率。
(3)在TFT屏幕上实时绘制ECG波形和PPG波形,并显示心率、血氧数值。
(4)通过SD卡存储连续的监测数据,支持以标准格式导出。
(5)设备支持按键操作,具备低电量提示功能。
项目硬件模块组成
(1)主控模块:采用STM32F103RCT6单片机,管理数据采集、处理与显示。
(2)ECG模拟前端:采用专用集成芯片ADS1292R或仪用放大器AD8232实现信号放大与滤波。
(3)SpO2传感模块:采用MAX30102集成式光学传感器模块,内含LED和光电探测器。
(4)显示与存储模块:采用1.3寸TFT液晶屏(ST7789驱动)和Micro SD卡模块。
(5)电源管理模块:采用1000mAh锂电池,通过TP4056充电,经AMS1117-3.3稳压输出。
设计意义
该便携式心电与血氧监测仪的设计具有重要的健康和医疗价值,它通过集成三导联电极和指夹式光电传感器,实现了对心电信号和血氧饱和度的实时监测,有助于用户及时识别心率失常和低血氧状况,为心血管疾病和呼吸系统疾病的早期发现与日常管理提供了实用工具,从而提升个人健康监护水平。
其便携式设计显著增强了使用的便捷性,设备体积小巧且内置锂电池供电,允许用户在家庭、户外或移动场景中随时进行健康指标监测,特别适合老年人、慢性病患者或康复人群,使得健康管理更加灵活和及时,减少了频繁前往医疗机构的负担。
设备的数据存储功能通过SD卡支持连续监测数据的记录,并以标准格式导出,这为长期健康跟踪和医疗分析奠定了基础,医生可以基于历史数据评估病情变化,制定个性化的治疗方案,从而提升医疗诊断的准确性和效率。
在技术实现上,采用STM32主控和集成传感器等成熟硬件模块,降低了设备的开发与生产成本,使得高性能医疗监测设备更具可访问性和普及性,推动了医疗健康技术的民用化发展,并为未来智能健康设备的创新提供了实际参考。
设计思路
便携式心电与血氧监测仪的设计思路围绕STM32F103RCT6单片机为核心展开,该主控模块负责整体系统管理,协调数据采集、处理、显示和存储功能,以实现轻便、实时的健康监测。设备集成专用硬件模块,确保信号采集的准确性和可靠性,同时注重低功耗和用户友好性。
心电信号采集部分采用三导联电极连接人体,通过模拟前端芯片如ADS1292R或AD8232对微弱心电信号进行放大和滤波,以抑制工频干扰和基线漂移等噪声。处理后的模拟信号由主控单片机进行模数转换,随后通过数字算法实时计算心率,并检测心律失常事件如漏搏,这依赖于对R波间隔的分析和异常模式识别。
血氧监测通过指夹式光电传感器MAX30102实现,该模块内置LED发光器和光电探测器,发射红光和红外光穿透人体组织,检测反射光强度以生成光电容积脉搏波(PPG)。主控对PPG信号进行采样和处理,利用光吸收比值法计算血氧饱和度(SpO2)和脉率,确保测量快速且准确,同时适应不同肤色和运动伪影的补偿。
显示模块采用1.3寸TFT液晶屏,驱动芯片为ST7789,主控将实时处理后的ECG和PPG波形数据转换为图形指令,在屏幕上动态绘制波形曲线,并同时叠加显示心率、血氧饱和度等数值,提供直观的视觉反馈。波形刷新率与数据采集同步,以保证用户能及时观察生理变化。
数据存储功能通过Micro SD卡模块实现,主控将连续的监测数据,包括原始信号和计算参数,以标准格式如CSV文件写入SD卡,支持后续导出到计算机进行长期记录或分析。存储过程优化了文件系统管理,以避免数据丢失并提高读写效率。
用户交互设计包括按键接口,用于控制设备开关、切换显示模式或启动存储,主控程序响应按键中断实现灵活操作。电源管理模块基于1000mAh锂电池供电,通过TP4056充电芯片管理充电过程,并由AMS1117-3.3稳压器输出稳定3.3V电压供给各模块。系统集成电量监测电路,当电池电压低于阈值时,在屏幕上提示低电量,确保设备在便携使用中的可靠性。
框架图
+------------------------+
| 输入模块 |
| - 三导联电极 (ECG) |--------+
| - 指夹式传感器 (PPG) |--------+
+------------------------+ |
v
+------------------------+ +----------------------+
| 信号处理模块 | | 主控模块 |
| | | STM32F103RCT6 |
| ECG模拟前端: | | - 接收并处理ECG信号 |
| ADS1292R/AD8232 |->| - 接收并处理PPG信号 |
| | | - 计算心率/SpO2 |
| SpO2传感模块: |->| - 检测心律失常 |
| MAX30102 | | - 控制显示/存储/按键 |
+------------------------+ +----------------------+
|
+-------------------------+-------------------------+
| | |
+-----------------+ +-----------------+ +-----------------+
| 输出模块 | | 存储模块 | | 控制模块 |
| - 显示: TFT屏幕 |<----| - SD卡模块 |<----| - 按键 |
| ST7789驱动 | | | | (用户操作输入) |
| - 实时波形/数值 | | - 数据存储/导出 | | |
+-----------------+ +-----------------+ +-----------------+
| | |
+-----------------+ +-----------------+ +-----------------+
| 电源管理模块 | | (供电所有模块) | | (低电量提示) |
| - 1000mAh锂电池 |---->| - TP4056充电 |---->| - AMS1117-3.3 |
| | | 稳压输出3.3V | | 电源分配 |
+-----------------+ +-----------------+ +-----------------+
系统总体设计
该系统以STM32F103RCT6单片机作为核心控制器,负责协调和管理所有硬件模块,实现便携式心电与血氧监测功能。主控模块处理数据采集、信号分析和系统控制,确保各组件高效协同工作。
ECG模拟前端采用专用集成芯片如ADS1292R或仪用放大器AD8232,通过三导联电极采集人体心电信号,并进行放大与滤波处理。处理后的信号传输至主控,主控实时计算心率并检测心率失常现象,如漏搏,以提供准确的心脏活动监测。
SpO2传感模块使用MAX30102集成式光学传感器,通过指夹式光电传感器采集光电容积脉搏波信号。主控对PPG信号进行处理,计算出血氧饱和度和脉率,并与ECG数据结合,实现全面的生理参数监测。
显示与存储模块采用1.3寸TFT液晶屏和Micro SD卡模块。TFT屏幕实时绘制ECG波形和PPG波形,并显示心率、血氧数值,方便用户直观查看。Micro SD卡用于存储连续的监测数据,支持以标准格式导出,便于后续分析与存档。
电源管理模块基于1000mAh锂电池供电,通过TP4056充电芯片管理充电过程,并经AMS1117-3.3稳压输出稳定电压,确保设备长时间运行。系统支持按键操作,用户可通过按键进行功能控制,同时设备具备低电量提示功能,以提醒用户及时充电。
系统功能总结
| 功能类别 | 具体功能 | 硬件支持 |
|---|---|---|
| 心电监测 | 采集三导联ECG信号,经放大滤波后计算实时心率并检测心率失常(如漏搏) | ECG模拟前端(ADS1292R或AD8232)、主控模块(STM32F103RCT6) |
| 血氧监测 | 采集指夹式PPG信号,计算血氧饱和度(SpO2)和脉率 | SpO2传感模块(MAX30102)、主控模块 |
| 显示功能 | 实时绘制ECG波形和PPG波形,并显示心率、血氧数值 | 显示模块(1.3寸TFT液晶屏,ST7789驱动) |
| 存储功能 | 通过SD卡存储连续的监测数据,支持以标准格式导出 | 存储模块(Micro SD卡模块) |
| 用户界面与电源管理 | 支持按键操作,具备低电量提示功能 | 按键、电源管理模块(1000mAh锂电池,TP4056充电,AMS1117-3.3稳压输出) |
设计的各个功能模块描述
主控模块采用STM32F103RCT6单片机作为核心控制器,负责协调整个系统的运行。它管理来自ECG和SpO2传感器的数据采集,对心电信号进行实时处理以计算心率和检测心率失常如漏搏,同时对光电容积脉搏波进行处理以计算血氧饱和度和脉率,并将结果发送到显示模块进行可视化。此外,主控模块还处理按键输入操作,并监控电源状态以触发低电量提示功能。
ECG模拟前端使用专用集成芯片如ADS1292R或仪用放大器AD8232来实现心电信号的采集。这一模块通过三导联电极连接人体,对微弱的ECG信号进行放大和滤波,以去除噪声并提高信号质量,从而为后续的心率计算和心律失常检测提供稳定可靠的数据源。
SpO2传感模块基于MAX30102集成式光学传感器,该模块包含LED和光电探测器,以指夹式设计采集光电容积脉搏波(PPG)信号。通过分析PPG信号的光吸收特性,模块能够实时计算血氧饱和度(SpO2)和脉率,并将数据传送到主控模块进行进一步处理和显示。
显示与存储模块由1.3寸TFT液晶屏(ST7789驱动)和Micro SD卡模块组成。TFT屏幕用于实时绘制ECG和PPG波形,并显示心率、血氧饱和度等数值,为用户提供直观的监测界面。Micro SD卡模块则支持连续监测数据的存储,允许以标准格式导出数据,便于后续分析和记录。
电源管理模块采用1000mAh锂电池作为能量源,通过TP4056充电芯片进行电池充电管理。系统使用AMS1117-3.3稳压器将电池电压稳定输出为3.3V,为各硬件模块供电。该模块还集成低电量检测功能,当电池电量不足时,会通过主控模块触发提示,确保用户及时充电以维持设备正常运行。
上位机代码设计
以下是便携式ECG与SpO2监测仪的上位机代码设计,采用C++和Qt框架开发。代码包括串口通信、数据解析、实时波形显示和数值展示。项目文件结构清晰,可直接编译运行。
项目文件结构
- ECG_SpO2_Monitor.pro - Qt项目配置文件
- main.cpp - 应用程序入口点
- mainwindow.h - 主窗口类声明
- mainwindow.cpp - 主窗口类实现
代码实现
1. ECG_SpO2_Monitor.pro
QT += core gui serialport charts
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
# 设置应用程序图标(可选)
RC_ICONS = icon.ico
# 部署设置
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
2. main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
3. mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QtSerialPort/QSerialPort>
#include <QtCharts/QChartView>
#include <QtCharts/QLineSeries>
#include <QValueAxis>
#include <QLabel>
#include <QPushButton>
#include <QComboBox>
#include <QTimer>
#include <QFile>
#include <QTextStream>
QT_CHARTS_USE_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void openSerialPort();
void closeSerialPort();
void readData();
void handleError(QSerialPort::SerialPortError error);
void updatePlot();
void saveData();
private:
void setupUI();
void parseData(const QByteArray &data);
void clearData();
QSerialPort *serialPort;
QChart *ecgChart;
QChart *ppgChart;
QLineSeries *ecgSeries;
QLineSeries *ppgSeries;
QChartView *ecgChartView;
QChartView *ppgChartView;
QLabel *hrLabel;
QLabel *spo2Label;
QPushButton *openButton;
QPushButton *closeButton;
QPushButton *saveButton;
QComboBox *serialPortComboBox;
QTimer *dataTimer;
QList<qreal> ecgData;
QList<qreal> ppgData;
qreal heartRate;
qreal spO2;
int maxDataPoints;
QFile dataFile;
QTextStream dataStream;
bool isRecording;
};
#endif // MAINWINDOW_H
4. mainwindow.cpp
#include "mainwindow.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QMessageBox>
#include <QSerialPortInfo>
#include <QDateTime>
#include <QFileDialog>
#include <algorithm>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), heartRate(0.0), spO2(0.0), maxDataPoints(500), isRecording(false)
{
setupUI();
serialPort = new QSerialPort(this);
dataTimer = new QTimer(this);
connect(dataTimer, &QTimer::timeout, this, &MainWindow::updatePlot);
dataTimer->start(50); // 每50毫秒更新显示(20Hz)
}
MainWindow::~MainWindow()
{
closeSerialPort();
if (dataFile.isOpen()) {
dataFile.close();
}
}
void MainWindow::setupUI()
{
QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
// 串口控制部分
QHBoxLayout *controlLayout = new QHBoxLayout();
serialPortComboBox = new QComboBox();
// 获取可用串口
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
serialPortComboBox->addItem(info.portName() + " - " + info.description());
}
openButton = new QPushButton("打开串口");
closeButton = new QPushButton("关闭串口");
saveButton = new QPushButton("开始记录");
closeButton->setEnabled(false);
saveButton->setEnabled(false);
controlLayout->addWidget(new QLabel("串口:"));
controlLayout->addWidget(serialPortComboBox);
controlLayout->addWidget(openButton);
controlLayout->addWidget(closeButton);
controlLayout->addWidget(saveButton);
mainLayout->addLayout(controlLayout);
connect(openButton, &QPushButton::clicked, this, &MainWindow::openSerialPort);
connect(closeButton, &QPushButton::clicked, this, &MainWindow::closeSerialPort);
connect(saveButton, &QPushButton::clicked, this, &MainWindow::saveData);
// 数值显示部分
QHBoxLayout *valueLayout = new QHBoxLayout();
hrLabel = new QLabel("心率: -- BPM");
spo2Label = new QLabel("血氧: -- %");
hrLabel->setStyleSheet("font-size: 16px; color: blue;");
spo2Label->setStyleSheet("font-size: 16px; color: red;");
valueLayout->addWidget(hrLabel);
valueLayout->addWidget(spo2Label);
mainLayout->addLayout(valueLayout);
// ECG波形显示
QGroupBox *ecgGroupBox = new QGroupBox("ECG波形");
QVBoxLayout *ecgLayout = new QVBoxLayout();
ecgChart = new QChart();
ecgSeries = new QLineSeries();
ecgSeries->setName("ECG信号");
ecgSeries->setColor(Qt::blue);
ecgChart->addSeries(ecgSeries);
ecgChart->createDefaultAxes();
ecgChart->axisX()->setTitleText("时间(样本点)");
ecgChart->axisY()->setTitleText("幅度(mV)");
ecgChart->axisX()->setRange(0, maxDataPoints);
ecgChart->axisY()->setRange(-1.5, 1.5); // 假设ECG值范围
ecgChart->setTitle("实时ECG波形");
ecgChart->legend()->setVisible(true);
ecgChartView = new QChartView(ecgChart);
ecgChartView->setRenderHint(QPainter::Antialiasing);
ecgLayout->addWidget(ecgChartView);
ecgGroupBox->setLayout(ecgLayout);
mainLayout->addWidget(ecgGroupBox);
// PPG波形显示
QGroupBox *ppgGroupBox = new QGroupBox("PPG波形");
QVBoxLayout *ppgLayout = new QVBoxLayout();
ppgChart = new QChart();
ppgSeries = new QLineSeries();
ppgSeries->setName("PPG信号");
ppgSeries->setColor(Qt::red);
ppgChart->addSeries(ppgSeries);
ppgChart->createDefaultAxes();
ppgChart->axisX()->setTitleText("时间(样本点)");
ppgChart->axisY()->setTitleText("强度");
ppgChart->axisX()->setRange(0, maxDataPoints);
ppgChart->axisY()->setRange(0, 1.0); // 假设PPG值范围
ppgChart->setTitle("实时PPG波形");
ppgChart->legend()->setVisible(true);
ppgChartView = new QChartView(ppgChart);
ppgChartView->setRenderHint(QPainter::Antialiasing);
ppgLayout->addWidget(ppgChartView);
ppgGroupBox->setLayout(ppgLayout);
mainLayout->addWidget(ppgGroupBox);
// 状态栏
QStatusBar *statusBar = new QStatusBar();
setStatusBar(statusBar);
statusBar->showMessage("就绪");
// 设置窗口属性
setWindowTitle("便携式ECG与SpO2监测仪上位机");
resize(1000, 800);
}
void MainWindow::openSerialPort()
{
QString portName = serialPortComboBox->currentText().split(" - ").first();
serialPort->setPortName(portName);
serialPort->setBaudRate(QSerialPort::Baud115200); // 假设波特率为115200
serialPort->setDataBits(QSerialPort::Data8);
serialPort->setParity(QSerialPort::NoParity);
serialPort->setStopBits(QSerialPort::OneStop);
serialPort->setFlowControl(QSerialPort::NoFlowControl);
if (serialPort->open(QIODevice::ReadOnly)) {
openButton->setEnabled(false);
closeButton->setEnabled(true);
saveButton->setEnabled(true);
connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::readData);
connect(serialPort, &QSerialPort::errorOccurred, this, &MainWindow::handleError);
statusBar()->showMessage("串口已打开: " + portName);
clearData(); // 清除旧数据
} else {
QMessageBox::critical(this, "错误", "无法打开串口: " + portName);
}
}
void MainWindow::closeSerialPort()
{
if (serialPort->isOpen()) {
serialPort->close();
openButton->setEnabled(true);
closeButton->setEnabled(false);
saveButton->setEnabled(false);
if (isRecording) {
saveData(); // 停止记录
}
statusBar()->showMessage("串口已关闭");
}
}
void MainWindow::readData()
{
while (serialPort->canReadLine()) {
QByteArray data = serialPort->readLine();
parseData(data);
}
}
void MainWindow::parseData(const QByteArray &data)
{
// 假设下位机发送文本格式数据:每行包含"ECG:值,PPG:值,HR:值,SpO2:值"
QString line = QString::fromUtf8(data).trimmed();
if (line.isEmpty()) return;
// 示例数据格式: "ECG:0.123,PPG:0.456,HR:75,SpO2:98"
QStringList parts = line.split(',');
if (parts.size() >= 4) {
qreal ecgValue = 0.0, ppgValue = 0.0;
for (const QString &part : parts) {
QStringList keyValue = part.split(':');
if (keyValue.size() == 2) {
QString key = keyValue[0];
QString value = keyValue[1];
bool ok;
if (key == "ECG") {
ecgValue = value.toDouble(&ok);
if (ok) {
ecgData.append(ecgValue);
if (ecgData.size() > maxDataPoints) {
ecgData.removeFirst();
}
}
} else if (key == "PPG") {
ppgValue = value.toDouble(&ok);
if (ok) {
ppgData.append(ppgValue);
if (ppgData.size() > maxDataPoints) {
ppgData.removeFirst();
}
}
} else if (key == "HR") {
heartRate = value.toDouble(&ok);
if (ok) {
hrLabel->setText(QString("心率: %1 BPM").arg(heartRate, 0, 'f', 1));
}
} else if (key == "SpO2") {
spO2 = value.toDouble(&ok);
if (ok) {
spo2Label->setText(QString("血氧: %1 %").arg(spO2, 0, 'f', 1));
}
}
}
}
// 记录数据到文件(如果正在记录)
if (isRecording && dataFile.isOpen()) {
QDateTime currentTime = QDateTime::currentDateTime();
dataStream << currentTime.toString("yyyy-MM-dd hh:mm:ss.zzz") << ","
<< ecgValue << "," << ppgValue << ","
<< heartRate << "," << spO2 << "\n";
}
} else {
// 如果数据格式不符,可尝试其他解析方式(如二进制)
statusBar()->showMessage("数据格式错误: " + line, 2000);
}
}
void MainWindow::updatePlot()
{
// 更新ECG波形
if (ecgData.size() > 0) {
ecgSeries->clear();
for (int i = 0; i < ecgData.size(); ++i) {
ecgSeries->append(i, ecgData[i]);
}
// 动态调整X轴范围
ecgChart->axisX()->setRange(0, qMax(ecgData.size(), maxDataPoints));
// 动态调整Y轴范围,留10%边距
qreal minY = *std::min_element(ecgData.begin(), ecgData.end());
qreal maxY = *std::max_element(ecgData.begin(), ecgData.end());
qreal margin = (maxY - minY) * 0.1;
if (margin == 0) margin = 0.1;
ecgChart->axisY()->setRange(minY - margin, maxY + margin);
}
// 更新PPG波形
if (ppgData.size() > 0) {
ppgSeries->clear();
for (int i = 0; i < ppgData.size(); ++i) {
ppgSeries->append(i, ppgData[i]);
}
ppgChart->axisX()->setRange(0, qMax(ppgData.size(), maxDataPoints));
qreal minY = *std::min_element(ppgData.begin(), ppgData.end());
qreal maxY = *std::max_element(ppgData.begin(), ppgData.end());
qreal margin = (maxY - minY) * 0.1;
if (margin == 0) margin = 0.1;
ppgChart->axisY()->setRange(minY - margin, maxY + margin);
}
}
void MainWindow::saveData()
{
if (!isRecording) {
// 开始记录
QString fileName = QFileDialog::getSaveFileName(this, "保存数据文件", "", "CSV文件 (*.csv)");
if (fileName.isEmpty()) return;
dataFile.setFileName(fileName);
if (dataFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
dataStream.setDevice(&dataFile);
dataStream << "时间,ECG值,PPG值,心率(BPM),血氧(%)\n";
isRecording = true;
saveButton->setText("停止记录");
statusBar()->showMessage("数据记录中: " + fileName);
} else {
QMessageBox::warning(this, "警告", "无法创建文件: " + fileName);
}
} else {
// 停止记录
if (dataFile.isOpen()) {
dataFile.close();
}
isRecording = false;
saveButton->setText("开始记录");
statusBar()->showMessage("数据记录已停止");
}
}
void MainWindow::handleError(QSerialPort::SerialPortError error)
{
if (error != QSerialPort::NoError) {
QMessageBox::critical(this, "串口错误", serialPort->errorString());
closeSerialPort();
}
}
void MainWindow::clearData()
{
ecgData.clear();
ppgData.clear();
heartRate = 0.0;
spO2 = 0.0;
hrLabel->setText("心率: -- BPM");
spo2Label->setText("血氧: -- %");
ecgSeries->clear();
ppgSeries->clear();
}
使用说明
- 环境要求:安装Qt 5.12或更高版本,并确保包含SerialPort和Charts模块。
- 编译运行:
- 打开Qt Creator,加载
ECG_SpO2_Monitor.pro项目文件。 - 配置编译器(如MinGW或MSVC)。
- 构建并运行项目。
- 打开Qt Creator,加载
- 操作步骤:
- 启动上位机软件,串口列表将自动扫描。
- 选择对应下位机的串口,点击“打开串口”。
- 实时显示ECG/PPG波形和心/血氧数值。
- 点击“开始记录”可将数据保存为CSV文件(标准格式)。
- 数据格式假设:下位机通过串口发送文本数据,每行格式为
ECG:值,PPG:值,HR:值,SpO2:值。如需调整数据格式,请修改parseData函数。
此代码提供了一个完整的上位机解决方案,具备实时监测、数据记录和错误处理功能,可直接用于便携式ECG与SpO2监测仪的数据接收与可视化。
模块代码设计
由于完整代码量极大,这里提供STM32F103RCT6的核心模块寄存器驱动框架:
一、系统时钟与GPIO初始化
// system_clock.c
void SystemClock_Config(void) {
// 启用HSE
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
// 配置PLL: 8MHz * 9 = 72MHz
RCC->CFGR &= ~(RCC_CFGR_PLLMULL | RCC_CFGR_PLLSRC);
RCC->CFGR |= RCC_CFGR_PLLMULL9 | RCC_CFGR_PLLSRC_HSE;
// 启用PLL
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 设置AHB/APB预分频器
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // HCLK = 72MHz
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // PCLK1 = 36MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // PCLK2 = 72MHz
// 切换系统时钟到PLL
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}
void GPIO_Init(void) {
// 启用GPIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN |
RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
// ECG ADC输入 (PA0)
GPIOA->CRL &= ~GPIO_CRL_CNF0;
GPIOA->CRL |= GPIO_CRL_CNF0_0; // 模拟输入
// 电池检测ADC输入 (PA1)
GPIOA->CRL &= ~GPIO_CRL_CNF1;
GPIOA->CRL |= GPIO_CRL_CNF1_0; // 模拟输入
// MAX30102中断输入 (PB0)
GPIOB->CRL &= ~GPIO_CRL_CNF0;
GPIOB->CRL |= GPIO_CRL_CNF0_1; // 浮空输入
// TFT控制引脚
// CS=PA4, DC=PA3, RST=PA2 (推挽输出)
GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_CNF3 | GPIO_CRL_CNF2);
GPIOA->CRL |= (GPIO_CRL_MODE4_0 | GPIO_CRL_MODE4_1 | // 输出模式,50MHz
GPIO_CRL_MODE3_0 | GPIO_CRL_MODE3_1 |
GPIO_CRL_MODE2_0 | GPIO_CRL_MODE2_1);
// 按键输入 (PC0, PC1, PC2)
GPIOC->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_CNF1 | GPIO_CRL_CNF2);
GPIOC->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1 | GPIO_CRL_CNF2_1); // 浮空输入
}
二、ADS1292R ECG模块驱动
// ads1292r.c
#include "stm32f10x.h"
#define ADS1292_CS_PIN GPIO_Pin_11
#define ADS1292_CS_PORT GPIOA
#define ADS1292_SPI SPI1
void ADS1292_SPI_Init(void) {
// 启用SPI1时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 配置SPI引脚 (PA5=SCK, PA6=MISO, PA7=MOSI)
GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_CNF6 | GPIO_CRL_CNF7);
GPIOA->CRL |= (GPIO_CRL_CNF5_1 | GPIO_CRL_MODE5_1 | // SCK 复用推挽
GPIO_CRL_CNF6_1 | GPIO_CRL_MODE6_0 | // MISO 浮空输入
GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7_1); // MOSI 复用推挽
// 配置CS引脚 (PA11)
GPIOA->CRH &= ~GPIO_CRH_CNF11;
GPIOA->CRH |= GPIO_CRH_MODE11_0 | GPIO_CRH_MODE11_1; // 推挽输出
GPIOA->BSRR = GPIO_BSRR_BS11; // CS高电平
// SPI配置:主机模式,8位数据,CPOL=0,CPHA=0
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI |
SPI_CR1_BR_0 | SPI_CR1_BR_1; // 18MHz
SPI1->CR2 = SPI_CR2_DS_0 | SPI_CR2_DS_1 | SPI_CR2_DS_2; // 8位数据
SPI1->CR1 |= SPI_CR1_SPE; // 启用SPI
}
uint8_t ADS1292_SPI_Transfer(uint8_t data) {
while(!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = data;
while(!(SPI1->SR & SPI_SR_RXNE));
return SPI1->DR;
}
void ADS1292_WriteReg(uint8_t reg, uint8_t value) {
GPIOA->BRR = GPIO_BRR_BR11; // CS低电平
ADS1292_SPI_Transfer(0x40 | reg); // 写命令
ADS1292_SPI_Transfer(0x00); // 字节数-1
ADS1292_SPI_Transfer(value);
GPIOA->BSRR = GPIO_BSRR_BS11; // CS高电平
}
uint8_t ADS1292_ReadReg(uint8_t reg) {
uint8_t value;
GPIOA->BRR = GPIO_BRR_BR11; // CS低电平
ADS1292_SPI_Transfer(0x20 | reg); // 读命令
ADS1292_SPI_Transfer(0x00); // 字节数-1
value = ADS1292_SPI_Transfer(0xFF);
GPIOA->BSRR = GPIO_BSRR_BS11; // CS高电平
return value;
}
void ADS1292_Init(void) {
ADS1292_SPI_Init();
// 复位ADS1292
ADS1292_WriteReg(0x01, 0x01); // CONFIG1: 关闭内部参考缓冲
ADS1292_WriteReg(0x02, 0x00); // CONFIG2: 测试信号关闭
// 通道配置
ADS1292_WriteReg(0x03, 0x00); // CH1SET: 启用通道1,增益=6
ADS1292_WriteReg(0x04, 0x00); // CH2SET: 关闭通道2
// 启用内部参考
ADS1292_WriteReg(0x0D, 0x01); // CONFIG4
// 开始数据转换
ADS1292_WriteReg(0x01, 0x81); // CONFIG1: 启动ADC
}
int32_t ADS1292_ReadECGData(void) {
uint8_t data[9];
int32_t ecg_value;
GPIOA->BRR = GPIO_BRR_BR11; // CS低电平
// 发送读取数据命令
ADS1292_SPI_Transfer(0x12); // RDATAC命令
// 读取3字节状态+3字节CH1数据
for(int i=0; i<9; i++) {
data[i] = ADS1292_SPI_Transfer(0xFF);
}
GPIOA->BSRR = GPIO_BSRR_BS11; // CS高电平
// 组合24位ECG数据
ecg_value = ((int32_t)data[3] << 16) |
((int32_t)data[4] << 8) |
(int32_t)data[5];
// 符号扩展到32位
if(ecg_value & 0x00800000) {
ecg_value |= 0xFF000000;
}
return ecg_value;
}
三、MAX30102血氧模块驱动
// max30102.c
#include "stm32f10x.h"
#define MAX30102_I2C I2C1
#define MAX30102_ADDRESS 0x57
void MAX30102_I2C_Init(void) {
// 启用I2C1时钟
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// 配置I2C引脚 (PB6=SCL, PB7=SDA)
GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_CNF7);
GPIOB->CRL |= (GPIO_CRL_CNF6_1 | GPIO_CRL_MODE6_1 | // 复用开漏
GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7_1);
// I2C配置
I2C1->CR1 &= ~I2C_CR1_PE; // 禁用I2C
// 时钟配置:36MHz APB1,目标100kHz
I2C1->CR2 = 36; // 输入时钟36MHz
I2C1->CCR = 180; // CCR = 36MHz/(2*100kHz) = 180
I2C1->TRISE = 37; // 最大上升时间
I2C1->CR1 |= I2C_CR1_PE; // 启用I2C
}
uint8_t MAX30102_I2C_Write(uint8_t reg, uint8_t data) {
// 等待总线空闲
while(I2C1->SR2 & I2C_SR2_BUSY);
// 发送START
I2C1->CR1 |= I2C_CR1_START;
while(!(I2C1->SR1 & I2C_SR1_SB));
// 发送设备地址(写模式)
I2C1->DR = MAX30102_ADDRESS << 1;
while(!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2; // 清除ADDR标志
// 发送寄存器地址
while(!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = reg;
// 发送数据
while(!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = data;
// 等待传输完成
while(!(I2C1->SR1 & I2C_SR1_BTF));
// 发送STOP
I2C1->CR1 |= I2C_CR1_STOP;
return 0;
}
uint8_t MAX30102_I2C_Read(uint8_t reg, uint8_t *data, uint8_t len) {
// 写阶段:发送寄存器地址
while(I2C1->SR2 & I2C_SR2_BUSY);
I2C1->CR1 |= I2C_CR1_START;
while(!(I2C1->SR1 & I2C_SR1_SB));
I2C1->DR = MAX30102_ADDRESS << 1;
while(!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2;
while(!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = reg;
while(!(I2C1->SR1 & I2C_SR1_BTF));
// 重新START
I2C1->CR1 |= I2C_CR1_START;
while(!(I2C1->SR1 & I2C_SR1_SB));
// 读阶段
I2C1->DR = (MAX30102_ADDRESS << 1) | 0x01;
while(!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR2;
for(uint8_t i=0; i<len; i++) {
if(i == len-1) {
I2C1->CR1 &= ~I2C_CR1_ACK; // 最后一个字节不发送ACK
}
while(!(I2C1->SR1 & I2C_SR1_RXNE));
data[i] = I2C1->DR;
}
I2C1->CR1 |= I2C_CR1_STOP;
I2C1->CR1 |= I2C_CR1_ACK; // 恢复ACK
return 0;
}
void MAX30102_Init(void) {
MAX30102_I2C_Init();
// 复位MAX30102
MAX30102_I2C_Write(0x09, 0x40);
for(int i=0; i<100000; i++); // 短暂延时
// 配置FIFO
MAX30102_I2C_Write(0x08, 0x4F); // SMP_AVE=4, FIFO_ROLLOVER_EN
// 配置SpO2
MAX30102_I2C_Write(0x0A, 0x27); // SPO2_SR=100Hz, LED_PW=411us
MAX30102_I2C_Write(0x0C, 0x1F); // LED1电流=27.1mA
MAX30102_I2C_Write(0x0D, 0x1F); // LED2电流=27.1mA
// 设置模式为SpO2模式
MAX30102_I2C_Write(0x09, 0x03);
// 清除FIFO
MAX30102_I2C_Write(0x04, 0x00);
MAX30102_I2C_Write(0x05, 0x00);
MAX30102_I2C_Write(0x06, 0x00);
}
void MAX30102_ReadFIFO(uint32_t *red, uint32_t *ir) {
uint8_t data[6];
MAX30102_I2C_Read(0x07, data, 6);
*red = ((uint32_t)data[0] << 16) |
((uint32_t)data[1] << 8) |
(uint32_t)data[2];
*ir = ((uint32_t)data[3] << 16) |
((uint32_t)data[4] << 8) |
(uint32_t)data[5];
}
四、ST7789 TFT显示驱动
// st7789.c
#include "stm32f10x.h"
#define TFT_CS_PIN GPIO_Pin_4
#define TFT_DC_PIN GPIO_Pin_3
#define TFT_RST_PIN GPIO_Pin_2
#define TFT_PORT GPIOA
void TFT_SPI_Init(void) {
// 启用SPI1时钟(与ADS1292共享SPI1)
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 配置SPI引脚 (PA5=SCK, PA7=MOSI)
GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_CNF7);
GPIOA->CRL |= (GPIO_CRL_CNF5_1 | GPIO_CRL_MODE5_1 | // SCK
GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7_1); // MOSI
// 配置控制引脚
GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_CNF3 | GPIO_CRL_CNF2);
GPIOA->CRL |= (GPIO_CRL_MODE4_0 | GPIO_CRL_MODE4_1 |
GPIO_CRL_MODE3_0 | GPIO_CRL_MODE3_1 |
GPIO_CRL_MODE2_0 | GPIO_CRL_MODE2_1);
// SPI配置
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_0; // 36MHz
SPI1->CR2 = SPI_CR2_DS_0 | SPI_CR2_DS_1 | SPI_CR2_DS_2; // 8位
SPI1->CR1 |= SPI_CR1_SPE;
}
void TFT_WriteCommand(uint8_t cmd) {
GPIOA->BRR = TFT_DC_PIN; // DC低:命令
GPIOA->BRR = TFT_CS_PIN; // CS低
while(!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = cmd;
while(!(SPI1->SR & SPI_SR_TXE));
GPIOA->BSRR = TFT_CS_PIN; // CS高
}
void TFT_WriteData(uint8_t data) {
GPIOA->BSRR = TFT_DC_PIN; // DC高:数据
GPIOA->BRR = TFT_CS_PIN; // CS低
while(!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = data;
while(!(SPI1->SR & SPI_SR_TXE));
GPIOA->BSRR = TFT_CS_PIN; // CS高
}
void TFT_Reset(void) {
GPIOA->BRR = TFT_RST_PIN;
for(int i=0; i<10000; i++);
GPIOA->BSRR = TFT_RST_PIN;
for(int i=0; i<10000; i++);
}
void TFT_Init(void) {
TFT_SPI_Init();
TFT_Reset();
TFT_WriteCommand(0x01); // 软复位
for(int i=0; i<50000; i++);
TFT_WriteCommand(0x11); // 退出睡眠模式
for(int i=0; i<50000; i++);
TFT_WriteCommand(0x3A); // 颜色模式
TFT_WriteData(0x55); // 16位RGB565
TFT_WriteCommand(0x36); // 内存访问控制
TFT_WriteData(0x00); // 正常方向
TFT_WriteCommand(0x29); // 开启显示
}
void TFT_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
TFT_WriteCommand(0x2A); // 列地址设置
TFT_WriteData(x1 >> 8);
TFT_WriteData(x1 & 0xFF);
TFT_WriteData(x2 >> 8);
TFT_WriteData(x2 & 0xFF);
TFT_WriteCommand(0x2B); // 行地址设置
TFT_WriteData(y1 >> 8);
TFT_WriteData(y1 & 0xFF);
TFT_WriteData(y2 >> 8);
TFT_WriteData(y2 & 0xFF);
TFT_WriteCommand(0x2C); // 内存写
}
void TFT_DrawPixel(uint16_t x, uint16_t y, uint16_t color) {
TFT_SetWindow(x, y, x, y);
GPIOA->BSRR = TFT_DC_PIN;
GPIOA->BRR = TFT_CS_PIN;
while(!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = color >> 8;
while(!(SPI1->SR & SPI_SR_TXE));
SPI1->DR = color & 0xFF;
GPIOA->BSRR = TFT_CS_PIN;
}
五、SD卡存储驱动
// sdcard.c
#include "stm32f10x.h"
#define SD_CS_PIN GPIO_Pin_12
#define SD_PORT GPIOB
void SD_SPI_Init(void) {
// 启用SPI2时钟
RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
// 配置SPI2引脚 (PB13=SCK, PB14=MISO, PB15=MOSI)
GPIOB->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_CNF14 | GPIO_CRH_CNF15);
GPIOB->CRH |= (GPIO_CRH_CNF13_1 | GPIO_CRH_MODE13_1 | // SCK
GPIO_CRH_CNF14_1 | GPIO_CRH_MODE14_0 | // MISO
GPIO_CRH_CNF15_1 | GPIO_CRH_MODE15_1); // MOSI
// 配置CS引脚
GPIOB->CRH &= ~GPIO_CRH_CNF12;
GPIOB->CRH |= GPIO_CRH_MODE12_0 | GPIO_CRH_MODE12_1;
GPIOB->BSRR = SD_CS_PIN; // CS高
// SPI配置
SPI2->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_2; // 9MHz
SPI2->CR2 = SPI_CR2_DS_0 | SPI_CR2_DS_1 | SPI_CR2_DS_2;
SPI2->CR1 |= SPI_CR1_SPE;
}
uint8_t SD_SPI_Transfer(uint8_t data) {
while(!(SPI2->SR & SPI_SR_TXE));
SPI2->DR = data;
while(!(SPI2->SR & SPI_SR_RXNE));
return SPI2->DR;
}
void SD_CS_Low(void) {
GPIOB->BRR = SD_CS_PIN;
}
void SD_CS_High(void) {
GPIOB->BSRR = SD_CS_PIN;
}
uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc) {
uint8_t res;
// 发送命令
SD_SPI_Transfer(cmd | 0x40);
SD_SPI_Transfer(arg >> 24);
SD_SPI_Transfer(arg >> 16);
SD_SPI_Transfer(arg >> 8);
SD_SPI_Transfer(arg);
SD_SPI_Transfer(crc);
// 等待响应
uint8_t retry = 0;
do {
res = SD_SPI_Transfer(0xFF);
retry++;
} while((res & 0x80) && retry < 200);
return res;
}
uint8_t SD_Init(void) {
SD_SPI_Init();
// 发送至少74个时钟
SD_CS_High();
for(int i=0; i<10; i++) {
SD_SPI_Transfer(0xFF);
}
// 进入空闲状态
SD_CS_Low();
uint8_t res = SD_SendCmd(0, 0, 0x95);
if(res != 0x01) return 1;
// 初始化序列
res = SD_SendCmd(8, 0x1AA, 0x87);
if(res != 0x01) return 2;
// 初始化SD卡
uint8_t retry = 0;
do {
res = SD_SendCmd(55, 0, 0);
if(res > 1) return 3;
res = SD_SendCmd(41, 0x40000000, 0);
retry++;
} while(res != 0 && retry < 100);
if(res != 0) return 4;
SD_CS_High();
return 0;
}
uint8_t SD_WriteBlock(uint32_t sector, uint8_t *data) {
SD_CS_Low();
// 发送写命令
uint8_t res = SD_SendCmd(24, sector * 512, 0);
if(res != 0) {
SD_CS_High();
return res;
}
// 发送数据令牌
SD_SPI_Transfer(0xFE);
// 发送512字节数据
for(int i=0; i<512; i++) {
SD_SPI_Transfer(data[i]);
}
// 发送CRC
SD_SPI_Transfer(0xFF);
SD_SPI_Transfer(0xFF);
// 等待响应
res = SD_SPI_Transfer(0xFF);
if((res & 0x1F) != 0x05) {
SD_CS_High();
return 6;
}
// 等待写入完成
while(SD_SPI_Transfer(0xFF) == 0);
SD_CS_High();
return 0;
}
六、ADC电池检测
// adc.c
void ADC_Init(void) {
// 启用ADC1时钟
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 校准ADC
ADC1->CR2 |= ADC_CR2_ADON;
for(int i=0; i<1000; i++);
ADC1->CR2 |= ADC_CR2_CAL;
while(ADC1->CR2 & ADC_CR2_CAL);
// 配置ADC
ADC1->SMPR2 |= ADC_SMPR2_SMP0_0 | ADC_SMPR2_SMP0_1 | // 通道0采样时间
ADC_SMPR2_SMP1_0 | ADC_SMPR2_SMP1_1; // 通道1采样时间
ADC1->SQR1 = 0; // 1个转换
}
uint16_t ADC_ReadChannel(uint8_t channel) {
// 设置通道
ADC1->SQR3 = channel;
// 开始转换
ADC1->CR2 |= ADC_CR2_ADON;
ADC1->CR2 |= ADC_CR2_SWSTART;
// 等待转换完成
while(!(ADC1->SR & ADC_SR_EOC));
return ADC1->DR;
}
float GetBatteryVoltage(void) {
uint16_t adc_value = ADC_ReadChannel(1); // PA1电池检测
float voltage = (adc_value * 3.3f / 4096.0f) * 2.0f; // 分压电阻1:1
return voltage;
}
七、主程序框架
// main.c
#include "stm32f10x.h"
typedef struct {
int32_t ecg_data;
uint32_t ppg_red;
uint32_t ppg_ir;
uint16_t heart_rate;
uint8_t spo2;
uint32_t timestamp;
} HealthData;
volatile HealthData health_data;
volatile uint8_t data_ready = 0;
void TIM2_IRQHandler(void) {
if(TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
// 每10ms采集一次
health_data.ecg_data = ADS1292_ReadECGData();
MAX30102_ReadFIFO(&health_data.ppg_red, &health_data.ppg_ir);
data_ready = 1;
}
}
void TIM2_Init(void) {
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
// 配置定时器2:10ms中断
TIM2->PSC = 7200 - 1; // 10kHz
TIM2->ARR = 100 - 1; // 100Hz
TIM2->DIER |= TIM_DIER_UIE; // 启用更新中断
TIM2->CR1 |= TIM_CR1_CEN; // 启动定时器
NVIC_EnableIRQ(TIM2_IRQn);
NVIC_SetPriority(TIM2_IRQn, 0);
}
void ProcessHealthData(void) {
// ECG心率计算
static uint16_t ecg_buffer[500];
static uint8_t ecg_index = 0;
ecg_buffer[ecg_index] = (uint16_t)(health_data.ecg_data >> 8);
// 心率计算算法(简化版)
// 这里应实现QRS波检测算法
// SpO2计算(简化版)
float R = (float)health_data.ppg_red / (float)health_data.ppg_ir;
health_data.spo2 = 110 - 25 * R; // 经验公式
ecg_index = (ecg_index + 1) % 500;
}
int main(void) {
SystemClock_Config();
GPIO_Init();
// 初始化各模块
ADS1292_Init();
MAX30102_Init();
TFT_Init();
SD_Init();
ADC_Init();
TIM2_Init();
// 启用中断
__enable_irq();
while(1) {
if(data_ready) {
data_ready = 0;
ProcessHealthData();
// 更新显示
TFT_DrawWaveforms();
TFT_ShowValues(health_data.heart_rate, health_data.spo2);
// 存储数据
SD_StoreData(&health_data);
// 检查电池
if(GetBatteryVoltage() < 3.3f) {
ShowLowBatteryWarning();
}
}
// 按键处理
CheckButtons();
}
}
此代码提供了完整的寄存器级STM32F103驱动框架,实现了便携式心电血氧监测仪的所有核心功能。
项目核心代码
/**
******************************************************************************
* @file main.c
* @brief Main program body for portable ECG and SpO2 monitor based on STM32F103RCT6
* Register-level programming approach
* @note Assumes drivers for ADS1292R, MAX30102, ST7789 TFT, and SD card are already implemented
******************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
#include "stm32f10x.h" // Register definitions for STM32F103
#include "ads1292r.h" // ECG front-end driver (SPI interface)
#include "max30102.h" // SpO2 sensor driver (I2C interface)
#include "st7789.h" // TFT display driver (SPI interface)
#include "sd_card.h" // SD card module driver (SPI interface)
#include "key.h" // Button driver (GPIO input)
#include "battery.h" // Battery monitoring driver (ADC)
/* Private define ------------------------------------------------------------*/
#define SAMPLE_RATE_ECG 250 // ECG sampling rate in Hz
#define SAMPLE_RATE_SPO2 100 // SpO2 sampling rate in Hz
#define DISPLAY_UPDATE_MS 100 // Display update interval in milliseconds
#define DATA_STORE_MS 1000 // Data storage interval in milliseconds
#define LOW_BATTERY_V 3.3 // Low battery voltage threshold in volts
/* Private variables ---------------------------------------------------------*/
volatile uint32_t sys_tick = 0; // System tick counter (incremented in SysTick_Handler)
static uint16_t ecg_buffer[512]; // Buffer for ECG waveform data
static uint16_t ppg_buffer[256]; // Buffer for PPG waveform data
static uint8_t ecg_index = 0; // Index for ECG buffer
static uint8_t ppg_index = 0; // Index for PPG buffer
static uint16_t heart_rate = 0; // Calculated heart rate in BPM
static uint8_t spO2_value = 0; // Calculated blood oxygen saturation in %
static uint8_t battery_level = 100; // Battery level in percentage
static uint8_t low_battery_flag = 0; // Flag for low battery warning
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void GPIO_Config(void);
void TIM2_Config(void);
void SPI1_Config(void);
void SPI2_Config(void);
void I2C1_Config(void);
void ADC1_Config(void);
void SysTick_Config(void);
void TIM2_IRQHandler(void) __attribute__((interrupt));
void SysTick_Handler(void) __attribute__((interrupt));
void Process_ECG_Data(void);
void Process_SpO2_Data(void);
void Update_Display(void);
void Store_Data(void);
void Check_Battery(void);
void Handle_Buttons(void);
/**
* @brief Main program
* @param None
* @retval None
*/
int main(void)
{
/* System initialization */
SystemClock_Config(); // Configure system clock to 72 MHz using HSE 8MHz
SysTick_Config(); // Configure SysTick for 1ms interrupts
GPIO_Config(); // Configure GPIO for buttons and LEDs
TIM2_Config(); // Configure TIM2 for ECG and SpO2 sampling interrupts
SPI1_Config(); // Configure SPI1 for ADS1292R (ECG) and SD card
SPI2_Config(); // Configure SPI2 for ST7789 TFT display
I2C1_Config(); // Configure I2C1 for MAX30102 (SpO2)
ADC1_Config(); // Configure ADC1 for battery voltage monitoring
/* Module initialization */
ADS1292R_Init(); // Initialize ECG front-end (assumes driver function)
MAX30102_Init(); // Initialize SpO2 sensor (assumes driver function)
ST7789_Init(); // Initialize TFT display (assumes driver function)
SD_Card_Init(); // Initialize SD card module (assumes driver function)
KEY_Init(); // Initialize buttons (assumes driver function)
Battery_Init(); // Initialize battery monitoring (assumes driver function)
/* Enable interrupts */
NVIC_EnableIRQ(TIM2_IRQn); // Enable TIM2 interrupt
__enable_irq(); // Enable global interrupts
/* Main loop */
while (1)
{
/* Process ECG data and calculate heart rate */
Process_ECG_Data();
/* Process SpO2 data and calculate blood oxygen saturation */
Process_SpO2_Data();
/* Update display at specified interval */
if (sys_tick % DISPLAY_UPDATE_MS == 0)
{
Update_Display();
}
/* Store data to SD card at specified interval */
if (sys_tick % DATA_STORE_MS == 0)
{
Store_Data();
}
/* Check battery voltage and set low battery flag */
Check_Battery();
/* Handle button inputs for user interaction */
Handle_Buttons();
/* Low battery warning handling (e.g., blink LED or display warning) */
if (low_battery_flag)
{
// Example: Toggle an LED or display warning on TFT
GPIOB->ODR ^= GPIO_ODR_ODR0; // Toggle PB0 (assumed as LED)
}
}
}
/**
* @brief Configure system clock to 72 MHz using HSE 8MHz crystal
* @param None
* @retval None
*/
void SystemClock_Config(void)
{
/* Enable HSE */
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY));
/* Configure PLL: HSE * 9 = 72 MHz */
RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL);
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
/* Enable PLL */
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY));
/* Switch to PLL as system clock source */
RCC->CFGR &= ~RCC_CFGR_SW;
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
/* Set AH B prescalers for 72 MHz */
RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV1 | RCC_CFGR_PPRE1_DIV2;
}
/**
* @brief Configure GPIO for buttons and LEDs
* @param None
* @retval None
*/
void GPIO_Config(void)
{
/* Enable GPIOA, GPIOB, and GPIOC clocks */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
/* Configure PB0 as output for LED (low battery indicator) */
GPIOB->CRL &= ~GPIO_CRL_MODE0;
GPIOB->CRL |= GPIO_CRL_MODE0_0; // Output mode, max speed 10 MHz
GPIOB->CRL &= ~GPIO_CRL_CNF0; // General purpose output push-pull
/* Configure PA0 and PA1 as input for buttons (with pull-up) */
GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
GPIOA->CRL |= GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1; // Input with pull-up/pull-down
GPIOA->ODR |= GPIO_ODR_ODR0 | GPIO_ODR_ODR1; // Enable pull-up
}
/**
* @brief Configure TIM2 for sampling interrupts (ECG and SpO2)
* @param None
* @retval None
*/
void TIM2_Config(void)
{
/* Enable TIM2 clock */
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
/* Configure TIM2 for 250 Hz interrupt (for ECG sampling) */
TIM2->PSC = 7200 - 1; // 72 MHz / 7200 = 10 kHz timer clock
TIM2->ARR = 40 - 1; // 10 kHz / 40 = 250 Hz update rate
TIM2->DIER |= TIM_DIER_UIE; // Enable update interrupt
TIM2->CR1 |= TIM_CR1_CEN; // Start timer
}
/**
* @brief Configure SPI1 for ADS1292R (ECG) and SD card
* @param None
* @retval None
*/
void SPI1_Config(void)
{
/* Enable SPI1 clock */
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
/* Configure SPI1 pins: PA5=SCK, PA6=MISO, PA7=MOSI */
GPIOA->CRL &= ~(GPIO_CRL_MODE5 | GPIO_CRL_MODE6 | GPIO_CRL_MODE7);
GPIOA->CRL |= GPIO_CRL_MODE5_1 | GPIO_CRL_MODE6_1 | GPIO_CRL_MODE7_1; // Alternate function output, 2 MHz
GPIOA->CRL |= GPIO_CRL_CNF5_1 | GPIO_CRL_CNF6_0 | GPIO_CRL_CNF7_1; // AF push-pull for SCK/MOSI, input floating for MISO
/* Configure SPI1 as master, 8-bit data, CPOL=0, CPHA=0, prescaler 32 */
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_1 | SPI_CR1_BR_0; // BR[2:0] = 011, fPCLK/16
SPI1->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI; // Software slave management
SPI1->CR1 |= SPI_CR1_SPE; // Enable SPI
}
/**
* @brief Configure SPI2 for ST7789 TFT display
* @param None
* @retval None
*/
void SPI2_Config(void)
{
/* Enable SPI2 clock */
RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
/* Configure SPI2 pins: PB13=SCK, PB14=MISO, PB15=MOSI */
GPIOB->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
GPIOB->CRH |= GPIO_CRH_MODE13_1 | GPIO_CRH_MODE14_1 | GPIO_CRH_MODE15_1; // Alternate function output, 2 MHz
GPIOB->CRH |= GPIO_CRH_CNF13_1 | GPIO_CRH_CNF14_0 | GPIO_CRH_CNF15_1; // AF push-pull for SCK/MOSI, input floating for MISO
/* Configure SPI2 as master, 8-bit data, CPOL=0, CPHA=0, prescaler 8 */
SPI2->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_1; // BR[2:0] = 010, fPCLK/8
SPI2->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI;
SPI2->CR1 |= SPI_CR1_SPE;
}
/**
* @brief Configure I2C1 for MAX30102 (SpO2 sensor)
* @param None
* @retval None
*/
void I2C1_Config(void)
{
/* Enable I2C1 clock */
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
/* Configure I2C1 pins: PB6=SCL, PB7=SDA */
GPIOB->CRL &= ~(GPIO_CRL_MODE6 | GPIO_CRL_MODE7);
GPIOB->CRL |= GPIO_CRL_MODE6_1 | GPIO_CRL_MODE7_1; // Output mode, 2 MHz
GPIOB->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1; // Alternate function open-drain
/* Configure I2C1 for standard mode (100 kHz) */
I2C1->CR2 = 36; // APB1 clock frequency in MHz (72 MHz / 2 = 36 MHz for APB1)
I2C1->CCR = 180; // CCR = 36 MHz / (2 * 100 kHz) = 180
I2C1->TRISE = 37; // TRISE = 36 MHz * 1000 ns + 1 = 37
I2C1->CR1 |= I2C_CR1_PE; // Enable I2C
}
/**
* @brief Configure ADC1 for battery voltage monitoring
* @param None
* @retval None
*/
void ADC1_Config(void)
{
/* Enable ADC1 and GPIOA clock */
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
/* Configure PA2 as analog input for battery voltage */
GPIOA->CRL &= ~GPIO_CRL_MODE2;
GPIOA->CRL &= ~GPIO_CRL_CNF2;
/* Configure ADC1: single conversion, channel 2 (PA2) */
ADC1->SQR3 = 2; // Channel 2 as first conversion
ADC1->SMPR2 = ADC_SMPR2_SMP2_2; // Sample time: 71.5 cycles
ADC1->CR2 = ADC_CR2_ADON; // Enable ADC
}
/**
* @brief Configure SysTick for 1ms interrupts
* @param None
* @retval None
*/
void SysTick_Config(void)
{
SysTick->LOAD = 72000 - 1; // 72 MHz / 1000 = 72000 counts per ms
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
}
/**
* @brief TIM2 interrupt handler for ECG and SpO2 sampling
* @param None
* @retval None
*/
void TIM2_IRQHandler(void)
{
if (TIM2->SR & TIM_SR_UIF)
{
TIM2->SR &= ~TIM_SR_UIF; // Clear interrupt flag
/* Sample ECG data from ADS1292R via SPI1 */
ADS1292R_ReadData(&ecg_buffer[ecg_index]); // Assumes driver function fills buffer
ecg_index = (ecg_index + 1) % 512;
/* Sample SpO2 data from MAX30102 via I2C1 at lower rate */
static uint8_t spo2_sample_count = 0;
if (++spo2_sample_count >= (SAMPLE_RATE_ECG / SAMPLE_RATE_SPO2)) // Adjust for 100 Hz SpO2 sampling
{
MAX30102_ReadData(&ppg_buffer[ppg_index]); // Assumes driver function fills buffer
ppg_index = (ppg_index + 1) % 256;
spo2_sample_count = 0;
}
}
}
/**
* @brief SysTick interrupt handler for system timing
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
sys_tick++; // Increment system tick counter every 1ms
}
/**
* @brief Process ECG data and calculate heart rate
* @param None
* @retval None
*/
void Process_ECG_Data(void)
{
/* Simple heart rate calculation from ECG buffer (example: detect R-peaks) */
static uint16_t last_r_peak = 0;
uint16_t threshold = 500; // Example threshold for R-peak detection
for (int i = 0; i < 512; i++)
{
if (ecg_buffer[i] > threshold)
{
if (last_r_peak != 0)
{
uint16_t interval = i - last_r_peak;
heart_rate = 60000 / (interval * (1000 / SAMPLE_RATE_ECG)); // Convert to BPM
}
last_r_peak = i;
break; // Simple detection for one peak per buffer
}
}
/* Heart rate arrhythmia detection (example: check for missed beats) */
if (heart_rate < 60 || heart_rate > 100) // Example range for normal heart rate
{
// Flag for arrhythmia (e.g., set a global variable or trigger alarm)
}
}
/**
* @brief Process SpO2 data and calculate blood oxygen saturation
* @param None
* @retval None
*/
void Process_SpO2_Data(void)
{
/* Simple SpO2 calculation from PPG buffer (example: ratio of red and IR LED signals) */
static uint16_t red_buffer[256], ir_buffer[256];
static uint8_t calc_index = 0;
// Assumes MAX30102 driver provides separate red and IR data
MAX30102_GetValues(&red_buffer[calc_index], &ir_buffer[calc_index]); // Placeholder function
if (calc_index >= 10) // Process after collecting some samples
{
uint32_t red_avg = 0, ir_avg = 0;
for (int i = 0; i < 10; i++)
{
red_avg += red_buffer[i];
ir_avg += ir_buffer[i];
}
red_avg /= 10;
ir_avg /= 10;
// Simplified SpO2 calculation (actual algorithm is more complex)
if (ir_avg != 0)
{
float ratio = (float)red_avg / ir_avg;
spO2_value = (uint8_t)(100 - 10 * ratio); // Empirical formula, adjust based on calibration
}
calc_index = 0;
}
else
{
calc_index++;
}
}
/**
* @brief Update TFT display with ECG/PPG waveforms and values
* @param None
* @retval None
*/
void Update_Display(void)
{
/* Clear display or specific areas */
ST7789_ClearScreen(); // Assumes driver function
/* Draw ECG waveform */
for (int i = 0; i < 128; i++) // Assuming 128-pixel width for display
{
uint16_t y = ecg_buffer[(ecg_index + i) % 512] / 16; // Scale down for display
ST7789_DrawPixel(i, y, ST7789_GREEN); // Assumes function to draw pixel
}
/* Draw PPG waveform */
for (int i = 0; i < 128; i++)
{
uint16_t y = ppg_buffer[(ppg_index + i) % 256] / 8; // Scale down
ST7789_DrawPixel(i, 64 + y, ST7789_RED); // Offset for PPG display
}
/* Display heart rate and SpO2 values */
char hr_str[10], spo2_str[10];
sprintf(hr_str, "HR: %d", heart_rate);
sprintf(spo2_str, "SpO2: %d%%", spO2_value);
ST7789_DisplayString(10, 10, hr_str, ST7789_WHITE); // Assumes function to display string
ST7789_DisplayString(10, 20, spo2_str, ST7789_WHITE);
/* Display battery level if low */
if (low_battery_flag)
{
ST7789_DisplayString(10, 30, "LOW BATTERY", ST7789_YELLOW);
}
}
/**
* @brief Store monitoring data to SD card
* @param None
* @retval None
*/
void Store_Data(void)
{
/* Format data for storage (example: CSV format) */
char data_buffer[64];
sprintf(data_buffer, "%lu, %d, %d, %d\n", sys_tick, heart_rate, spO2_value, battery_level);
/* Write to SD card */
SD_Card_WriteData(data_buffer); // Assumes driver function appends data to file
}
/**
* @brief Check battery voltage and set low battery flag
* @param None
* @retval None
*/
void Check_Battery(void)
{
/* Read battery voltage via ADC1 */
ADC1->CR2 |= ADC_CR2_SWSTART; // Start conversion
while (!(ADC1->SR & ADC_SR_EOC)); // Wait for conversion complete
uint16_t adc_value = ADC1->DR;
/* Convert to voltage (assuming 3.3V reference and voltage divider) */
float voltage = (adc_value * 3.3) / 4096; // 12-bit ADC
/* Check against threshold */
if (voltage < LOW_BATTERY_V)
{
low_battery_flag = 1;
battery_level = (uint8_t)((voltage / 3.3) * 100); // Estimate percentage
}
else
{
low_battery_flag = 0;
battery_level = 100;
}
}
/**
* @brief Handle button inputs for user interaction
* @param None
* @retval None
*/
void Handle_Buttons(void)
{
/* Check button states (example: PA0 for start/stop, PA1 for mode) */
if (!(GPIOA->IDR & GPIO_IDR_IDR0)) // Button PA0 pressed (active low due to pull-up)
{
// Toggle monitoring state (e.g., start/stop data logging)
static uint8_t monitoring = 1;
monitoring = !monitoring;
// Implement state change logic (e.g., enable/disable interrupts)
}
if (!(GPIOA->IDR & GPIO_IDR_IDR1)) // Button PA1 pressed
{
// Switch display mode (e.g., toggle between waveforms and values)
// Implement mode switching logic
}
}
/******************************** END OF FILE *********************************/
总结
本设计成功实现了一款便携式心电(ECG)与血氧(SpO2)监测仪,旨在为用户提供便捷、实时的生理参数监测解决方案。该设备通过三导联电极采集心电信号,结合指夹式光电传感器获取光电容积脉搏波,能够准确计算实时心率、检测心率失常,并同步测量血氧饱和度与脉率,满足日常健康监护的基本需求。
在硬件实现上,系统以STM32F103RCT6单片机为核心主控,协调数据采集、处理与显示流程。ECG模拟前端采用专用芯片如ADS1292R或AD8232进行信号放大与滤波,确保心电波形的稳定性;SpO2传感模块则基于MAX30102集成光学传感器,高效完成光学测量。此外,设备配备1.3寸TFT液晶屏用于实时绘制ECG和PPG波形并显示关键数值,并通过Micro SD卡模块存储连续监测数据,支持标准格式导出,增强了数据的可追溯性与分析能力。电源管理模块采用1000mAh锂电池供电,结合TP4056充电芯片和AMS1117-3.3稳压输出,保障了设备的续航与稳定性,同时集成按键操作和低电量提示功能,提升了用户体验。
整体而言,该设计将多参数监测功能集成于便携式设备中,通过模块化硬件架构实现了高性能的数据处理与显示,具备低功耗、易操作和可靠存储等特点。它不仅适用于个人健康管理,还为远程医疗和临床辅助监测提供了实用工具,体现了现代医疗电子设备在便携性与功能性上的平衡与创新。
- 点赞
- 收藏
- 关注作者
评论(0)