开发环境 : VS2017 社区版+QT 5.11.2 +插件qt-vsaddin-msvc2017-2.2.2
在上一篇文章中QtCharts绘制动态心电图[1]——初步应用中已经实现的心电图的动态实现,但是在实际情况中,数据通过串口或者网口传来,如果到来一次数据绘制一次,有可能有以下两种情况:
其实这里不想介绍了。网上有很多细致的讲解。。。
因为之前在STM32开发项目中也使用到了环形队列,所以这里环形队列的实现还是借鉴之前的实现方法。
主要是参考了这个网址STM32的串口软件FIFO
环形队列的声明
//******** fifo相关 ***********
typedef struct {
qint16 data_buf[FIFO_BUFFER_SIZE]; // FIFO buffer 16位有符号数
quint16 i_first; // index of oldest data byte in buffer
quint16 i_last; // index of newest data byte in buffer
quint16 num_bytes; // number of bytes currently in buffer
}sw_fifo_typedef;
sw_fifo_typedef ecg_data_fifo = { {0}, 0, 0, 0 }; // declare a receive software buffer
//------------- fifo中标志位的配置 ---------------
quint8 fifo_not_empty_flag; // this flag is automatically set and cleared by the software buffer
quint8 fifo_full_flag; // this flag is automatically set and cleared by the software buffer
quint8 fifo_ovf_flag; // this flag is not automatically cleared by the software buffer
往队列中写入数据函数
//*********** fifo环形队列相关 开始******************
void RealTimeEcg::ecgDataFifoIn(qint16 inputData) {
//
/* Explicitly clear the source of interrupt if necessary */
if (ecg_data_fifo.num_bytes == FIFO_BUFFER_SIZE) { // if the sw buffer is full
fifo_ovf_flag = 1; // set the overflow flag
//TODO fifo_ovf_flag,这个标志影响是否还写入,或者是清空已有数据,然后重新谢写入?
}
else if (ecg_data_fifo.num_bytes < FIFO_BUFFER_SIZE) { // if there's room in the sw buffer
ecg_data_fifo.data_buf[ecg_data_fifo.i_last] = inputData;/* enter pointer to UART rx hardware buffer here */ // store the received data as the newest data element in the sw buffer
/
ecg_data_fifo.i_last++; // increment the index of the most recently added element
ecg_data_fifo.num_bytes++; // increment the bytes counter
}
if (ecg_data_fifo.num_bytes == FIFO_BUFFER_SIZE) { // if sw buffer just filled up
fifo_full_flag = 1;
// set the FIFO full flag
}
if (ecg_data_fifo.i_last == FIFO_BUFFER_SIZE) { // if the index has reached the end of the buffer,
ecg_data_fifo.i_last = 0; // roll over the index counter
}
fifo_not_empty_flag = 1; // set received-data flag
}
接下来是读取队列中的数据
//如果在环形队列中没有数据,返回为0
qint16 RealTimeEcg::ecgDataFifoOut() {
//
/* Explicitly clear the source of interrupt if necessary */
qint16 outputData = 0;
if (ecg_data_fifo.num_bytes == FIFO_BUFFER_SIZE) { // if the sw buffer is full
fifo_full_flag = 0; // clear the buffer full flag because we are about to make room
}
if (ecg_data_fifo.num_bytes > 0) { // if data exists in the sw buffer
outputData = ecg_data_fifo.data_buf[ecg_data_fifo.i_first]; // place oldest data element in the buffer
ecg_data_fifo.i_first++; // increment the index of the oldest element
ecg_data_fifo.num_bytes--; // decrement the bytes counter
}
if (ecg_data_fifo.i_first == FIFO_BUFFER_SIZE) { // if the index has reached the end of the buffer,
ecg_data_fifo.i_first = 0; // roll over the index counter
}
if (ecg_data_fifo.num_bytes == 0) { // if no more data exists
fifo_not_empty_flag = 0; // clear flag
}
return outputData;
}
开发开发着就发现,其实QT有自带的队列,使用也要简便的多,不用去考虑那么多的细节实现。并且在实际情况中,并不会耗尽所有资源区。如果考虑实时性更好,就会考虑使用快、慢速两种状态对队列内数据进行绘制,保证未绘制数据控制在一定范围内,也就是控制延迟时间在一定范围内。
//QT中队列的定义
QQueue <qint16>m_EcgShortQueue;//中间是数据类型
//队列插入一个数
m_EcgShortQueue.enqueue(tempInt16);
//取出一个数
drawPoint = m_EcgShortQueue.dequeue();
可以看出QQueue的使用还是很简单方便的。
但是需要注意QQueue 的本质其实是一个list,所以在使用dequeue取出一个数的时候,必须要判断m_EcgShortQueue的size是否为0,不然程序会直接崩溃。(QT的list经常出现这些问题。。。基本每个使用list的地方都要记得判断)
if (m_EcgShortQueue.size() != 0)
{
drawPoint = m_EcgShortQueue.dequeue();
}
相对于前一篇,这里初始化就添加600个点(可能全部是是0),之后就不用判断是否是第一次进行数据绘制,直接替换对应点,减少系统消耗
ecgSeries->clear();
//提前添加完点,就不用考虑是否是第一次绘制,只用替换现有点的数据
for (int i = 0; i < AXIS_X_MAX_COUNTS; i++)
{
//ecgPointBuffer.append(QPointF(i, 0));
*ecgSeries << QPointF(i, 0);
}
在实际的工作中,一般是一定时间收到一个数据包,所以这里我们模拟500ms收到一个包
//模拟TCP数据到来的定时器
ecgWaveReadTimer = new QTimer(this);
connect(ecgWaveReadTimer, SIGNAL(timeout()), this, SLOT(oneTimeOutWriteToFifoAction()));
ecgWaveReadTimer->start(500);//500ms执行一次
然后是槽函数,将读出的数据加入队列中
//模拟500ms收到一个数据包 以后这部分就可以是TCP或者是串口接收到一次数据就加入一次缓存队列
void RealTimeEcg::oneTimeOutWriteToFifoAction() {
//需要从上一次队尾开始读取
for (int i = originListIndex; i < (125 + originListIndex); i++)
{
qint16 tempInt16 = originList.at(i).toInt();
m_EcgShortQueue.enqueue(tempInt16);
}
originListIndex += 125;
//如果剩下的数据不足以支撑下一次数据读取,就停止定时器
if ((originListIndex + 125) >= originListSize)
{
ecgWaveReadTimer->stop();
}
}
相对于前一篇,阅读源码发现了series的replace函数,所以这里对于数据的更新做了一点小修改,以为我们在初始化折线图的时候就添加600个点,所以这里我们只需要使用replace函数替换更改的点就可以了。
和之前类似,我们先初始化绘制定时器
//******绘制折线定时器***********
ecgWaveDrawTimer = new QTimer(this);
ecgWaveDrawTimer->setInterval(DRAW_ECG_INTERVAL);
ecgWaveDrawTimer->setTimerType(Qt::PreciseTimer);//精确
connect(ecgWaveDrawTimer, SIGNAL(timeout()), this, SLOT(oneTimeOutReadFromFifoAction()));
ecgWaveDrawTimer->start();
然后我们定义绘制函数。
//********* 读取队列中的数据,重复绘制数据 ****************
void RealTimeEcg::oneTimeOutReadFromFifoAction() {//一直进行绘制,不会停止,除非手动停止
qDebug() << QString("fifo中剩余的数据个数%1").arg(m_EcgShortQueue.size());
//环形队列如果内部没有数据的时候,fifoOut一直为0
qint16 drawPoint = 0;
if (axis_x_counts == AXIS_X_MAX_COUNTS)
{
axis_x_counts = 0;
}
if (m_EcgShortQueue.size() != 0)
{
drawPoint = m_EcgShortQueue.dequeue();
}
ecgSeries->replace(axis_x_counts, QPointF(axis_x_counts, drawPoint));//只替换当前数据点,不替换所有的数据
axis_x_counts++;
}
如果数据量很大,再加上Qt的定时器不是特别精确,采用这个简单案例的方案会导致缓存数据越来越多,所以本方法采用的是直接舍弃前面几秒的数据。这样就会造成部分数据丢失。
之前采用的优化方案是根据缓存数量修改Timer的间隔时间,保证所有数据都能进行绘制,具体方案如下:
- 当队列缓存数据数量小于一个值的时候,设置心电定时器间隔时间长一点,达到缓慢绘制效果
- 当队列缓存数据数量大于一个值的时候,设置心电定时器间隔时间短一点,达到快速绘制效果
但是其实现实中还有很多的问题,比如8ms进行一次绘制非常占用资源,比如QtCharts快速绘制的时候非常占用资源,如果需要快速显示多个心电窗格(比如大量心电数据的回看)等待时间非常长, 请看下一篇文章《QtCharts绘制动态心电图[3]——实时绘制的优化处理》(不知道多久能够完成- - 主要是要避开涉及项目的内容)。
程序下载链接
说明:程序提供的下载(本程序是利用VS2017+Qt 5.11.2进行编写,建议用相同环境打开,不同的Qt版本可能会导致编译不成功,最近时间挺紧张,后面有时间才可能再考虑转换成纯Qt版本)