1. 项目配置和主窗口
1.1 项目文件 (.pro)
qmake
QT += core gui charts serialport
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
SOURCES += \
main.cpp \
mainwindow.cpp \
serialportreader.cpp \
ecgchart.cpp
HEADERS += \
mainwindow.h \
serialportreader.h \
ecgchart.h
FORMS += \
mainwindow.ui
# 资源文件
RESOURCES += \
resources.qrc
1.2 主窗口头文件 (mainwindow.h)
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QValueAxis>
#include <QTimer>
#include <QFile>
#include <QDateTime>
#include <QMessageBox>
#include <QDebug>
#include "ecgchart.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
// 串口相关槽函数
void on_btnOpenPort_clicked();
void on_btnClosePort_clicked();
void on_btnRefreshPorts_clicked();
void on_btnStartRecord_clicked();
void on_btnStopRecord_clicked();
// 串口数据处理
void readSerialData();
void handleError(QSerialPort::SerialPortError error);
// 图表控制
void on_btnPause_clicked();
void on_btnClear_clicked();
void on_sliderSpeed_valueChanged(int value);
void on_sliderGain_valueChanged(int value);
// 滤波器设置
void on_cbFilter_currentIndexChanged(int index);
// 数据保存
void saveECGData();
private:
Ui::MainWindow *ui;
// 串口对象
QSerialPort *serialPort;
// 图表对象
ECGChart *ecgChart;
// 定时器
QTimer *dataTimer;
QTimer *saveTimer;
// 数据缓冲区
QVector<qreal> ecgDataBuffer;
QByteArray serialBuffer;
// 记录状态
bool isRecording;
QString currentRecordFile;
// 设置默认参数
void setupDefaultParameters();
// 初始化串口列表
void initSerialPorts();
// 初始化图表
void initChart();
// 处理接收到的数据
void processECGData(const QByteArray &data);
// 解析数据包
bool parseDataPacket(const QByteArray &packet, qint16 &ecgValue, qint16 &heartRate);
// 保存数据到文件
void writeDataToFile(const QString &data);
// 滤波处理
qreal applyFilter(qreal value);
// 移动平均滤波
qreal movingAverageFilter(qreal value);
// 中值滤波
qreal medianFilter(qreal value);
// IIR滤波
qreal iirFilter(qreal value);
// 滤波缓冲区
QVector<qreal> maBuffer;
QVector<qreal> medianBuffer;
// IIR滤波器系数
qreal iirAlpha; // 低通滤波系数
qreal iirLastValue;
};
#endif // MAINWINDOW_H
1.3 主窗口实现文件 (mainwindow.cpp)
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, serialPort(nullptr)
, isRecording(false)
, iirAlpha(0.1)
, iirLastValue(0.0)
{
ui->setupUi(this);
// 设置窗口标题
setWindowTitle("心电波形实时显示系统");
// 初始化UI
setupDefaultParameters();
// 初始化串口
initSerialPorts();
// 初始化图表
initChart();
// 初始化定时器
dataTimer = new QTimer(this);
connect(dataTimer, &QTimer::timeout, this, &MainWindow::readSerialData);
saveTimer = new QTimer(this);
connect(saveTimer, &QTimer::timeout, this, &MainWindow::saveECGData);
// 初始化滤波器缓冲区
maBuffer.resize(5, 0.0);
medianBuffer.resize(5, 0.0);
}
MainWindow::~MainWindow()
{
if (serialPort && serialPort->isOpen()) {
serialPort->close();
}
if (isRecording) {
writeDataToFile("--- Recording Stopped ---\n");
}
delete ui;
}
void MainWindow::setupDefaultParameters()
{
// 设置默认串口参数
ui->cbBaudRate->setCurrentText("115200");
ui->cbDataBits->setCurrentText("8");
ui->cbParity->setCurrentText("None");
ui->cbStopBits->setCurrentText("1");
ui->cbFlowControl->setCurrentText("None");
// 设置默认显示参数
ui->sliderSpeed->setValue(50);
ui->sliderGain->setValue(100);
// 设置默认滤波器
ui->cbFilter->setCurrentIndex(0);
// 初始化按钮状态
ui->btnOpenPort->setEnabled(true);
ui->btnClosePort->setEnabled(false);
ui->btnStartRecord->setEnabled(false);
ui->btnStopRecord->setEnabled(false);
ui->btnPause->setEnabled(false);
ui->btnClear->setEnabled(true);
}
void MainWindow::initSerialPorts()
{
ui->cbPortName->clear();
// 获取所有可用的串口
QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &port : ports) {
QString portInfo = QString("%1 - %2")
.arg(port.portName())
.arg(port.description());
ui->cbPortName->addItem(portInfo, port.portName());
}
if (ports.isEmpty()) {
ui->cbPortName->addItem("未找到串口设备");
ui->btnOpenPort->setEnabled(false);
}
}
void MainWindow::initChart()
{
// 创建心电图表
ecgChart = new ECGChart();
ecgChart->setAnimationOptions(QChart::NoAnimation);
ecgChart->setTheme(QChart::ChartThemeDark);
// 将图表添加到UI
QChartView *chartView = new QChartView(ecgChart);
chartView->setRenderHint(QPainter::Antialiasing);
ui->chartLayout->addWidget(chartView);
// 设置初始参数
ecgChart->setDisplaySpeed(50);
ecgChart->setGain(1.0);
// 连接图表信号
connect(ecgChart, &ECGChart::heartRateUpdated,
this, int hr {
ui->lblHeartRate->setText(QString::number(hr));
});
}
void MainWindow::on_btnRefreshPorts_clicked()
{
initSerialPorts();
}
void MainWindow::on_btnOpenPort_clicked()
{
if (serialPort && serialPort->isOpen()) {
return;
}
// 获取选中的串口名
QString portName = ui->cbPortName->currentData().toString();
if (portName.isEmpty()) {
QMessageBox::warning(this, "警告", "请选择有效的串口");
return;
}
// 创建串口对象
serialPort = new QSerialPort(this);
serialPort->setPortName(portName);
// 设置串口参数
serialPort->setBaudRate(ui->cbBaudRate->currentText().toInt());
serialPort->setDataBits(static_cast<QSerialPort::DataBits>(
ui->cbDataBits->currentText().toInt()));
serialPort->setParity(static_cast<QSerialPort::Parity>(
ui->cbParity->currentIndex()));
serialPort->setStopBits(static_cast<QSerialPort::StopBits>(
ui->cbStopBits->currentIndex() + 1));
serialPort->setFlowControl(static_cast<QSerialPort::FlowControl>(
ui->cbFlowControl->currentIndex()));
// 打开串口
if (serialPort->open(QIODevice::ReadWrite)) {
ui->statusbar->showMessage(QString("已连接到 %1").arg(portName), 3000);
// 连接信号槽
connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::readSerialData);
connect(serialPort, static_cast<void(QSerialPort::*)(QSerialPort::SerialPortError)>(&QSerialPort::error),
this, &MainWindow::handleError);
// 更新按钮状态
ui->btnOpenPort->setEnabled(false);
ui->btnClosePort->setEnabled(true);
ui->btnStartRecord->setEnabled(true);
ui->btnPause->setEnabled(true);
ui->btnRefreshPorts->setEnabled(false);
// 清空数据缓冲区
ecgDataBuffer.clear();
serialBuffer.clear();
// 开始定时读取
dataTimer->start(20); // 50Hz更新频率
} else {
QMessageBox::critical(this, "错误",
QString("无法打开串口 %1: %2")
.arg(portName)
.arg(serialPort->errorString()));
delete serialPort;
serialPort = nullptr;
}
}
void MainWindow::on_btnClosePort_clicked()
{
if (serialPort && serialPort->isOpen()) {
serialPort->close();
delete serialPort;
serialPort = nullptr;
dataTimer->stop();
ui->btnOpenPort->setEnabled(true);
ui->btnClosePort->setEnabled(false);
ui->btnStartRecord->setEnabled(false);
ui->btnStopRecord->setEnabled(false);
ui->btnPause->setEnabled(false);
ui->btnRefreshPorts->setEnabled(true);
ui->statusbar->showMessage("串口已关闭", 3000);
}
}
void MainWindow::readSerialData()
{
if (!serialPort || !serialPort->isOpen()) {
return;
}
// 读取所有可用数据
QByteArray newData = serialPort->readAll();
if (newData.isEmpty()) {
return;
}
// 添加到缓冲区
serialBuffer.append(newData);
// 处理完整的数据包
// 假设数据格式: 起始标志(0xAA 0x55) + ECG数据(2字节) + 心率(2字节) + 校验和(1字节)
while (serialBuffer.size() >= 7) { // 7字节数据包
// 查找起始标志
int startIndex = serialBuffer.indexOf("\xAA\x55");
if (startIndex == -1) {
// 没有找到起始标志,清空无效数据
serialBuffer.clear();
break;
}
if (startIndex > 0) {
// 移除起始标志前的无效数据
serialBuffer = serialBuffer.mid(startIndex);
}
if (serialBuffer.size() < 7) {
// 数据不完整,等待更多数据
break;
}
// 提取完整数据包
QByteArray packet = serialBuffer.left(7);
serialBuffer = serialBuffer.mid(7);
// 解析数据包
qint16 ecgValue = 0;
qint16 heartRate = 0;
if (parseDataPacket(packet, ecgValue, heartRate)) {
// 滤波处理
qreal filteredValue = applyFilter(ecgValue);
// 更新图表
ecgChart->addDataPoint(filteredValue);
// 显示心率
ui->lblHeartRate->setText(QString::number(heartRate));
// 保存数据
if (isRecording) {
QString dataLine = QString("%1,%2,%3\n")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss.zzz"))
.arg(ecgValue)
.arg(heartRate);
writeDataToFile(dataLine);
}
}
}
}
bool MainWindow::parseDataPacket(const QByteArray &packet, qint16 &ecgValue, qint16 &heartRate)
{
if (packet.size() != 7) {
return false;
}
// 检查起始标志
if (static_cast<quint8>(packet[0]) != 0xAA ||
static_cast<quint8>(packet[1]) != 0x55) {
return false;
}
// 解析ECG数据 (小端模式)
ecgValue = static_cast<qint16>(
(static_cast<quint8>(packet[2]) << 8) |
static_cast<quint8>(packet[3]));
// 解析心率数据
heartRate = static_cast<qint16>(
(static_cast<quint8>(packet[4]) << 8) |
static_cast<quint8>(packet[5]));
// 计算校验和 (简单的异或校验)
quint8 checksum = 0;
for (int i = 0; i < 6; ++i) {
checksum ^= static_cast<quint8>(packet[i]);
}
quint8 receivedChecksum = static_cast<quint8>(packet[6]);
return checksum == receivedChecksum;
}
void MainWindow::handleError(QSerialPort::SerialPortError error)
{
if (error == QSerialPort::ResourceError) {
QMessageBox::critical(this, "串口错误",
QString("串口发生错误: %1").arg(serialPort->errorString()));
on_btnClosePort_clicked();
}
}
void MainWindow::on_btnStartRecord_clicked()
{
if (!serialPort || !serialPort->isOpen()) {
return;
}
// 生成文件名
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss");
currentRecordFile = QString("ecg_data_%1.csv").arg(timestamp);
// 创建文件并写入表头
QString header = "Timestamp,ECG_Value,Heart_Rate\n";
writeDataToFile(header);
isRecording = true;
ui->btnStartRecord->setEnabled(false);
ui->btnStopRecord->setEnabled(true);
// 启动自动保存定时器
saveTimer->start(5000); // 每5秒自动保存
ui->statusbar->showMessage(QString("开始记录: %1").arg(currentRecordFile), 3000);
}
void MainWindow::on_btnStopRecord_clicked()
{
if (!isRecording) {
return;
}
isRecording = false;
ui->btnStartRecord->setEnabled(true);
ui->btnStopRecord->setEnabled(false);
saveTimer->stop();
// 写入结束标记
writeDataToFile("--- Recording Stopped ---\n");
ui->statusbar->showMessage(QString("记录已停止: %1").arg(currentRecordFile), 3000);
}
void MainWindow::saveECGData()
{
// 刷新文件缓冲区
if (isRecording) {
QFile file(currentRecordFile);
if (file.isOpen()) {
// 可以在这里添加额外的保存逻辑
}
}
}
void MainWindow::writeDataToFile(const QString &data)
{
QFile file(currentRecordFile);
if (file.open(QIODevice::Append | QIODevice::Text)) {
QTextStream stream(&file);
stream << data;
file.close();
}
}
void MainWindow::on_btnPause_clickedaggio()
{
static bool isPaused = false;
if (isPaused) {
// 恢复显示
if (serialPort && serialPort->isOpen()) {
dataTimer->start();
}
ecgChart->resume();
ui->btnPause->setText("暂停");
isPaused = false;
} else {
// 暂停显示
dataTimer->stop();
ecgChart->pause();
ui->btnPause->setText("继续");
isPaused = true;
}
}
void MainWindow::on_btnClear_clicked()
{
ecgChart->clear();
ecgDataBuffer.clear();
ui->lblHeartRate->setText("--");
}
void MainWindow::on_sliderSpeed_valueChanged(int value)
{
qreal speed = value / 100.0 * 5.0; // 0-5秒显示范围
ecgChart->setDisplaySpeed(value);
ui->lblSpeed->setText(QString("%1 s").arg(speed, 0, 'f', 1));
}
void MainWindow::on_sliderGain_valueChanged(int value)
{
qreal gain = value / 100.0;
ecgChart->setGain(gain);
ui->lblGain->setText(QString("x%1").arg(gain, 0, 'f', 2));
}
void MainWindow::on_cbFilter_currentIndexChanged(int index)
{
// 切换滤波器类型
// 0: 无滤波, 1: 移动平均, 2: 中值滤波, 3: IIR滤波
Q_UNUSED(index)
// 滤波器设置在applyFilter中处理
}
qreal MainWindow::applyFilter(qreal value)
{
int filterType = ui->cbFilter->currentIndex();
switch (filterType) {
case 1: // 移动平均滤波
return movingAverageFilter(value);
case 2: // 中值滤波
return medianFilter(value);
case 3: // IIR滤波
return iirFilter(value);
default: // 无滤波
return value;
}
}
qreal MainWindow::movingAverageFilter(qreal value)
{
// 更新缓冲区
for (int i = maBuffer.size() - 1; i > 0; --i) {
maBuffer[i] = maBuffer[i - 1];
}
maBuffer[0] = value;
// 计算平均值
qreal sum = 0;
for (const qreal &v : maBuffer) {
sum += v;
}
return sum / maBuffer.size();
}
qreal MainWindow::medianFilter(qreal value)
{
// 更新缓冲区
for (int i = medianBuffer.size() - 1; i > 0; --i) {
medianBuffer[i] = medianBuffer[i - 1];
}
medianBuffer[0] = value;
// 复制并排序
QVector<qreal> sortedBuffer = medianBuffer;
std::sort(sortedBuffer.begin(), sortedBuffer.end());
// 返回中值
return sortedBuffer[sortedBuffer.size() / 2];
}
qreal MainWindow::iirFilter(qreal value)
{
// 一阶IIR低通滤波器
qreal filtered = iirAlpha * value + (1 - iirAlpha) * iirLastValue;
iirLastValue = filtered;
return filtered;
}
2. 心电图表类
2.1 心电图表头文件 (ecgchart.h)
cpp
#ifndef ECGCHART_H
#define ECGCHART_H
#include <QObject>
#include <QChart>
#include <QLineSeries>
#include <QValueAxis>
#include <QTimer>
#include <QQueue>
QT_CHARTS_USE_NAMESPACE
class ECGChart : public QChart
{
Q_OBJECT
public:
explicit ECGChart(QGraphicsItem *parent = nullptr,
Qt::WindowFlags wFlags = Qt::WindowFlags());
~ECGChart();
// 添加数据点
void addDataPoint(qreal value);
// 清除数据
void clear();
// 暂停/继续
void pause();
void resume();
// 设置显示参数
void setDisplaySpeed(int speed); // 速度百分比
void setGain(qreal gain); // 增益系数
void setSampleRate(int rate); // 采样率(Hz)
// 设置显示范围
void setYRange(qreal min, qreal max);
signals:
void heartRateUpdated(int bpm);
void dataPointAdded(qreal value);
private slots:
void updateChart();
private:
// 图表系列
QLineSeries *ecgSeries;
// 坐标轴
QValueAxis *axisX;
QValueAxis *axisY;
// 数据缓冲区
QQueue<QPointF> dataPoints;
int maxDataPoints;
// 显示参数
qreal displayTime; // 显示的时间范围(秒)
qreal gain; // 增益
int sampleRate; // 采样率
// 控制参数
bool isPaused;
// 心率计算
QVector<qreal> rPeaks;
qreal lastRValue;
qreal rThreshold;
int rrInterval;
int heartRate;
// 定时器
QTimer *updateTimer;
// 初始化图表
void initChart();
// 检测R波
bool detectRWave(qreal value);
// 计算心率
void calculateHeartRate();
// 更新坐标轴范围
void updateAxisRange();
};
#endif // ECGCHART_H
2.2 心电图表实现文件 (ecgchart.cpp)
cpp
#include "ecgchart.h"
#include <QDebug>
ECGChart::ECGChart(QGraphicsItem *parent, Qt::WindowFlags wFlags)
: QChart(QChart::ChartTypeCartesian, parent, wFlags)
, displayTime(5.0)
, gain(1.0)
, sampleRate(200)
, isPaused(false)
, lastRValue(0.0)
, rThreshold(100.0)
, rrInterval(0)
, heartRate(0)
{
// 初始化图表
initChart();
// 设置最大数据点数
maxDataPoints = displayTime * sampleRate;
// 初始化定时器
updateTimer = new QTimer(this);
updateTimer->setInterval(50); // 20Hz更新频率
connect(updateTimer, &QTimer::timeout, this, &ECGChart::updateChart);
updateTimer->start();
}
ECGChart::~ECGChart()
{
updateTimer->stop();
}
void ECGChart::initChart()
{
// 创建系列
ecgSeries = new QLineSeries();
ecgSeries->setName("心电波形");
ecgSeries->setPen(QPen(Qt::green, 1.5));
// 添加系列到图表
addSeries(ecgSeries);
// 创建坐标轴
axisX = new QValueAxis();
axisY = new QValueAxis();
// 设置X轴
axisX->setTitleText("时间 (秒)");
axisX->setRange(0, displayTime);
axisX->setLabelFormat("%.1f");
axisX->setTickCount(6);
axisX->setGridLineVisible(true);
// 设置Y轴
axisY->setTitleText("幅值 (mV)");
axisY->setRange(-1.5, 1.5);
axisY->setTickCount(7);
axisY->setGridLineVisible(true);
// 添加坐标轴
addAxis(axisX, Qt::AlignBottom);
addAxis(axisY, Qt::AlignLeft);
// 关联系列和坐标轴
ecgSeries->attachAxis(axisX);
ecgSeries->attachAxis(axisY);
// 设置图表属性
setTitle("实时心电波形");
setAnimationOptions(QChart::NoAnimation);
legend()->setVisible(false);
// 设置边距
setMargins(QMargins(5, 5, 5, 5));
}
void ECGChart::addDataPoint(qreal value)
{
if (isPaused) {
return;
}
// 应用增益
qreal scaledValue = value * gain;
// 检测R波
if (detectRWave(scaledValue)) {
calculateHeartRate();
emit heartRateUpdated(heartRate);
}
// 添加数据点
qreal time = dataPoints.size() / static_cast<qreal>(sampleRate);
dataPoints.enqueue(QPointF(time, scaledValue));
// 保持数据点数不超过最大值
if (dataPoints.size() > maxDataPoints) {
dataPoints.dequeue();
}
// 发送信号
emit dataPointAdded(scaledValue);
}
void ECGChart::clear()
{
dataPoints.clear();
ecgSeries->clear();
rPeaks.clear();
lastRValue = 0.0;
rrInterval = 0;
heartRate = 0;
emit heartRateUpdated(0);
}
void ECGChart::pause()
{
isPaused = true;
}
void ECGChart::resume()
{
isPaused = false;
}
void ECGChart::setDisplaySpeed(int speed)
{
// 速度百分比转换为显示时间
displayTime = 0.5 + (speed / 100.0) * 9.5; // 0.5-10秒
maxDataPoints = displayTime * sampleRate;
// 移除多余的数据点
while (dataPoints.size() > maxDataPoints) {
dataPoints.dequeue();
}
}
void ECGChart::setGain(qreal newGain)
{
gain = newGain;
// 重新计算所有数据点
if (!dataPoints.isEmpty()) {
QQueue<QPointF> scaledPoints;
for (const QPointF &point : dataPoints) {
scaledPoints.enqueue(QPointF(point.x(), point.y() * gain));
}
dataPoints = scaledPoints;
}
}
void ECGChart::setSampleRate(int rate)
{
sampleRate = rate;
maxDataPoints = displayTime * sampleRate;
// 调整现有数据点的时间坐标
if (!dataPoints.isEmpty()) {
QQueue<QPointF> rescaledPoints;
qint32 index = 0;
for (const QPointF &point : dataPoints) {
qreal time = index / static_cast<qreal>(sampleRate);
rescaledPoints.enqueue(QPointF(time, point.y()));
index++;
}
dataPoints = rescaledPoints;
}
}
void ECGChart::setYRange(qreal min, qreal max)
{
axisY->setRange(min, max);
}
void ECGChart::updateChart()
{
if (isPaused || dataPoints.isEmpty()) {
return;
}
// 更新系列数据
ecgSeries->clear();
// 计算时间偏移
qreal currentTime = 0;
if (!dataPoints.isEmpty()) {
currentTime = dataPoints.last().x();
}
// 添加数据点到系列
for (const QPointF &point : dataPoints) {
// 调整X坐标,使其始终在显示范围内
qreal x = point.x() - (currentTime - displayTime);
if (x >= 0) {
ecgSeries->append(x, point.y());
}
}
// 更新坐标轴范围
updateAxisRange();
}
void ECGChart::updateAxisRange()
{
// 更新X轴范围
qreal maxTime = displayTime;
qreal minTime = 0;
axisX->setRange(minTime, maxTime);
// 自动调整Y轴范围
if (!dataPoints.isEmpty()) {
qreal minY = 1000;
qreal maxY = -1000;
for (const QPointF &point : dataPoints) {
if (point.y() < minY) minY = point.y();
if (point.y() > maxY) maxY = point.y();
}
// 添加一些边距
qreal margin = (maxY - minY) * 0.1;
if (margin < 0.1) margin = 0.1;
axisY->setRange(minY - margin, maxY + margin);
}
}
bool ECGChart::detectRWave(qreal value)
{
static bool isRising = false;
static int samplesSinceLastR = 0;
samplesSinceLastR++;
// 简单R波检测算法
if (value > rThreshold && value > lastRValue) {
isRising = true;
}
else if (isRising && value < lastRValue && value > rThreshold * 0.8) {
// 检测到峰值
isRising = false;
// 避免检测到太近的R波
if (samplesSinceLastR > sampleRate * 0.3) { // 最少300ms间隔
rPeaks.append(value);
rrInterval = samplesSinceLastR;
samplesSinceLastR = 0;
// 保持最近10个R波
if (rPeaks.size() > 10) {
rPeaks.removeFirst();
}
lastRValue = value;
return true;
}
}
lastRValue = value;
return false;
}
void ECGChart::calculateHeartRate()
{
if (rPeaks.size() < 2 || rrInterval == 0) {
heartRate = 0;
return;
}
// 计算平均RR间期
qreal avgRR = 0;
for (int i = 1; i < rPeaks.size(); i++) {
// 这里应该使用实际的时间差
// 简化:使用采样点间隔
avgRR += 1.0; // 实际应该用时间差
}
avgRR /= (rPeaks.size() - 1);
// 计算心率 (BPM = 60 / RR间期(秒))
if (avgRR > 0) {
heartRate = static_cast<int>(60.0 / (avgRR / sampleRate));
// 限制心率在合理范围内
if (heartRate < 30) heartRate = 30;
if (heartRate > 200) heartRate = 200;
}
}
3. 串口读取器类
3.1 串口读取器头文件 (serialportreader.h)
cpp
#ifndef SERIALPORTREADER_H
#define SERIALPORTREADER_H
#include <QObject>
#include <QSerialPort>
#include <QByteArray>
class SerialPortReader : public QObject
{
Q_OBJECT
public:
explicit SerialPortReader(QObject *parent = nullptr);
~SerialPortReader();
// 打开串口
bool openPort(const QString &portName,
qint32 baudRate = QSerialPort::Baud115200,
QSerialPort::DataBits dataBits = QSerialPort::Data8,
QSerialPort::Parity parity = QSerialPort::NoParity,
QSerialPort::StopBits stopBits = QSerialPort::OneStop,
QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl);
// 关闭串口
void closePort();
// 检查串口是否打开
bool isOpen() const;
// 发送数据
qint64 writeData(const QByteArray &data);
signals:
// 数据接收信号
void dataReceived(const QByteArray &data);
// 错误信号
void errorOccurred(const QString &errorString);
// 端口状态信号
void portOpened();
void portClosed();
private slots:
void handleReadyRead();
void handleError(QSerialPort::SerialPortError error);
private:
QSerialPort *serialPort;
QByteArray receiveBuffer;
// 数据包解析
void parseDataPackets();
};
#endif // SERIALPORTREADER_H
4. 界面文件 (mainwindow.ui)
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1200</width>
<height>800</height>
</rect>
</property>
<property name="windowTitle">
<string>心电波形实时显示系统</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>串口设置</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>串口:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cbPortName"/>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="btnRefreshPorts">
<property name="text">
<string>刷新</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>波特率:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cbBaudRate">
<item>
<property name="text">
<string>9600</string>
</property>
</item>
<item>
<property name="text">
<string>19200</string>
</property>
</item>
<item>
<property name="text">
<string>38400</string>
</property>
</item>
<item>
<property name="text">
<string>57600</string>
</property>
</item>
<item>
<property name="text">
<string>115200</string>
</property>
</item>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnOpenPort">
<property name="text">
<string>打开串口</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>数据位:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="cbDataBits">
<item>
<property name="text">
<string>5</string>
</property>
</item>
<item>
<property name="text">
<string>6</string>
</property>
</item>
<item>
<property name="text">
<string>7</string>
</property>
</item>
<item>
<property name="text">
<string>8</string>
</property>
</item>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="btnClosePort">
<property name="text">
<string>关闭串口</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>校验位:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="cbParity">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Even</string>
</property>
</item>
<item>
<property name="text">
<string>Odd</string>
</property>
</item>
<item>
<property name="text">
<string>Space</string>
</property>
</item>
<item>
<property name="text">
<string>Mark</string>
</property>
</item>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="btnStartRecord">
<property name="text">
<string>开始记录</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>停止位:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="cbStopBits">
<item>
<property name="text">
<string>1</string>
</property>
</item>
<item>
<property name="text">
<string>1.5</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
</widget>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="btnStopRecord">
<property name="text">
<string>停止记录</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>流控制:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="cbFlowControl">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>RTS/CTS</string>
</property>
</item>
<item>
<property name="text">
<string>XON/XOFF</string>
</property>
</item>
</widget>
</item>
<item row="5" column="2">
<widget class="QPushButton" name="btnPause">
<property name="text">
<string>暂停</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>显示控制</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>显示速度:</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="sliderSpeed">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblSpeed">
<property name="text">
<string>2.5 s</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>增益:</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="sliderGain">
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>500</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblGain">
<property name="text">
<string>x1.00</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>滤波器:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="cbFilter">
<item>
<property name="text">
<string>无滤波</string>
</property>
</item>
<item>
<property name="text">
<string>移动平均</string>
</property>
</item>
<item>
<property name="text">
<string>中值滤波</string>
</property>
</item>
<item>
<property name="text">
<string>IIR滤波</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnClear">
<property name="text">
<string>清除波形</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>心电参数</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>心率:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblHeartRate">
<property name="text">
<string>--</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>BPM</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>采样率:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblSampleRate">
<property name="text">
<string>200 Hz</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_13">
<property name="text">
<string>数据点:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblDataPoints">
<property name="text">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="chartLayout"/>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
5. 主程序 (main.cpp)
cpp
#include "mainwindow.h"
#include <QApplication>
#include <QStyleFactory>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置应用程序信息
QApplication::setApplicationName("ECG Waveform Monitor");
QApplication::setOrganizationName("Medical Systems");
QApplication::setApplicationVersion("1.0.0");
// 设置样式
qApp->setStyle(QStyleFactory::create("Fusion"));
// 设置调色板
QPalette palette;
palette.setColor(QPalette::Window, QColor(53, 53, 53));
palette.setColor(QPalette::WindowText, Qt::white);
palette.setColor(QPalette::Base, QColor(25, 25, 25));
palette.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
palette.setColor(QPalette::ToolTipBase, Qt::white);
palette.setColor(QPalette::ToolTipText, Qt::white);
palette.setColor(QPalette::Text, Qt::white);
palette.setColor(QPalette::Button, QColor(53, 53, 53));
palette.setColor(QPalette::ButtonText, Qt::white);
palette.setColor(QPalette::BrightText, Qt::red);
palette.setColor(QPalette::Link, QColor(42, 130, 218));
palette.setColor(QPalette::Highlight, QColor(42, 130, 218));
palette.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(palette);
MainWindow w;
w.show();
return a.exec();
}
6. STM32单片机端代码示例
c
// STM32端的心电数据发送代码
#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdio.h>
// ADC缓冲区
#define ADC_BUFFER_SIZE 200
uint16_t adc_buffer[ADC_BUFFER_SIZE];
// 心电数据包结构
typedef struct {
uint8_t start_flag[2]; // 起始标志: 0xAA 0x55
uint16_t ecg_value; // ECG值
uint16_t heart_rate; // 心率
uint8_t checksum; // 校验和
} ECG_Packet;
// 计算校验和
uint8_t calculate_checksum(const ECG_Packet* packet) {
uint8_t checksum = 0;
uint8_t* data = (uint8_t*)packet;
for(int i = 0; i < sizeof(ECG_Packet) - 1; i++) {
checksum ^= data[i];
}
return checksum;
}
// 发送心电数据包
void send_ecg_packet(UART_HandleTypeDef* huart, uint16_t ecg_value, uint16_t heart_rate) {
ECG_Packet packet;
// 设置起始标志
packet.start_flag[0] = 0xAA;
packet.start_flag[1] = 0x55;
// 设置数据
packet.ecg_value = ecg_value;
packet.heart_rate = heart_rate;
// 计算校验和
packet.checksum = calculate_checksum(&packet);
// 发送数据包
HAL_UART_Transmit(huart, (uint8_t*)&packet, sizeof(ECG_Packet), 100);
}
// 模拟心电数据生成
uint16_t generate_ecg_signal(uint32_t time_ms) {
static uint32_t last_time = 0;
static uint16_t ecg_value = 2048; // 中间值
// 模拟心电波形
uint32_t phase = time_ms % 1000; // 1秒周期
if(phase < 50) {
// P波
ecg_value = 2100 + (phase * 2);
} else if(phase >= 50 && phase < 100) {
// PR段
ecg_value = 2200;
} else if(phase >= 100 && phase < 150) {
// QRS波群
ecg_value = 2400 - ((phase - 100) * 8);
} else if(phase >= 150 && phase < 180) {
// ST段
ecg_value = 2000;
} else if(phase >= 180 && phase < 230) {
// T波
ecg_value = 2100 + ((phase - 180) * 2);
} else {
// 基线
ecg_value = 2048;
}
// 添加噪声
int16_t noise = (rand() % 100) - 50; // -50到+50的噪声
ecg_value += noise;
// 限制范围
if(ecg_value > 4095) ecg_value = 4095;
if(ecg_value < 0) ecg_value = 0;
return ecg_value;
}
// 主循环
int main(void) {
HAL_Init();
SystemClock_Config();
// 初始化外设
MX_GPIO_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
// 初始化ADC DMA
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
uint32_t last_send_time = 0;
uint16_t heart_rate = 72; // 模拟心率
while(1) {
uint32_t current_time = HAL_GetTick();
// 每20ms发送一次数据 (50Hz)
if(current_time - last_send_time >= 20) {
// 读取ADC值
uint16_t adc_value = adc_buffer[0]; // 使用第一个采样值
// 或者生成模拟信号
// adc_value = generate_ecg_signal(current_time);
// 发送数据
send_ecg_packet(&huart1, adc_value, heart_rate);
last_send_time = current_time;
}
// 模拟心率变化
if(current_time % 10000 == 0) { // 每10秒变化一次
heart_rate = 60 + (rand() % 40); // 60-100 BPM
}
}
}
参考代码 利用QT读取单片机发送的串口数据 www.youwenfan.com/contentcsv/71024.html
7. 编译和运行说明
7.1 环境要求
- QT 5.12 或更高版本
- QT Charts 模块
- QT SerialPort 模块
- C++11 兼容编译器
7.2 编译步骤
- 创建QT Widgets Application项目
- 在.pro文件中添加QT Charts和SerialPort模块
- 将上述代码文件添加到项目中
- 配置UI文件
- 编译运行
7.3 使用说明
- 连接STM32开发板到电脑串口
- 运行QT程序
- 选择正确的串口号和波特率
- 点击"打开串口"按钮
- 调节显示参数和滤波器设置
- 点击"开始记录"保存数据
- 观察实时心电波形和心率显示
8. 功能特点
- 实时显示:实时显示心电波形,支持暂停/继续
- 多滤波器:提供多种滤波器选项
- 参数调节:可调节显示速度和增益
- 数据记录:支持数据记录为CSV格式
- 心率检测:简单的R波检测和心率计算
- 串口通信:支持多种串口参数设置
- 美观界面:采用暗色主题,保护眼睛
9. 注意事项
- 确保单片机发送的数据格式与程序解析格式一致
- 根据实际心电信号调整增益和滤波器参数
- 采样率需要与单片机端匹配
- 长时间运行注意内存管理
- 实际应用中需要更精确的R波检测算法