基本信息
SDK: QT5.9

编程语言: C++

应用场景: 并发, 低延时,低资源场景下的 数据处理方案

基本流程: 数据计算->数据更新->数据发送

功能特点:
①可通过UI配置 多个数据计算单元 并发或者 顺序执行,

②执行QT程序的工控机硬件资源有限, 数据的更新和数据的发送确保必要的时候执行

③精准计时实现固定频率从串口发送数据到下位机处理

方案描述:
计算单元

单个计算单元的计算量并不大, 线程处理不适用, 采用定时器+事件循环的方式实现类似C++20协程的概念来实现

计算单元通过 定时器m_fadeTimer连接updateDmxValues()函数, 计时器周期性触发计算数据的函数

m_fadeTimer = new QTimer(this);

connect(m_fadeTimer, &QTimer::timeout, this, &Cue::updateDmxValues);

更新单元

更新单元通过定时器来精准计时,周期性更新由多个计算单元处理后得出的有效数据, 将更新的数据保存到新的变量中, 作为一帧数据为数据的发送做准备,

// 设置定时器,每秒触发50次(20ms间隔)

updateTimer.setInterval(20);

connect(&updateTimer, &QTimer::timeout, this, &impl_seq::sendDmxUpdate);

发送单元

当数据更新后, 开启发送定时器, 周期性的发送由 数据更新单元保存的当前需要发送的数据帧, 并确保当有数据更新的时候, 数据发送保持开启, 没有数据更新的时候, 数据发送功能关闭, 节约资源.

m_sendTimer = new QTimer(this);

bool connected = connect(m_sendTimer, &QTimer::timeout, this, &Impl_comm::sendSeqPacket);

qDebug() << “Signal connected:” << connected;

关键点解析

多个数据计算单元可能并发执行, 在数据计算期间, 数据更新定时器要cover整个计算过程, 数据计算频次为50次/秒,等价于一个计算单元20ms计算一次数值(数值随时间渐变), 为确保数据准确(最终发送出去的数据和计算数据一致(不丢数据, 不错数据) , 数据更新函数也要保证至少每20ms触发一次, 数据发送单元同理, 即数据计算单元计算数据, 数据更新单元捕获最新数据, 数据发送单元稳定发送数据流, 保证数据流输出低延迟的前提下, 尽量减小资源的占用.

点击添加图片描述(最多60个字)
编辑

具体实现

计算单元部分示例及说明

void Cue::startInDelay()

{

QTimer::singleShot(cueAttr.inDelay * 1000, this, &Cue::startFade);

}

void Cue::startFade()

{

emit sendFlagAddSignal(); //触发数据发送开始信号

    m_fadeTimer->start(20); // 50Hz update rate

//原子整数类型变量updateTimerFlag, 确保多个计算单元对标志位的操作不会丢失(每有一个计算单元执行,该标志位累加1)

    int a = m_implSeq->updateTimerFlag.fetchAndAddRelaxed(1);

    if(a == 0)

    {

//只有当updateTimerFlag为0的时候才会触发更新计时器,即当前计算单元是第一个执行的.

        m_implSeq->updateTimer.start();

    }

}

//计算单元计算数据

void Cue::updateDmxValues()

{

// QElapsedTimer类型的成员m_elapsedTimer, 区别于qtimer, 精准计算时间,处理数据

double elapsedSeconds = (m_elapsedTimer.elapsed() - m_pausedElapsed) / 1000.0 - cueAttr.inDelay;

m_progress = qMin(elapsedSeconds / cueAttr.fade, 1.0);

//数值随时间渐变, 输出具体的计算值

for (auto universeIt = cueTarg.constBegin(); universeIt != cueTarg.constEnd(); ++universeIt) 

{

    int universe = universeIt.key();

    for (auto channelIt = universeIt.value().constBegin(); channelIt != universeIt.value().constEnd(); ++channelIt) 

{

        int channel = channelIt.key();

        quint8 initialValue = m_initialValues[universe][channel];

        quint8 targetValue = channelIt.value();

        quint8 currentValue =   + (targetValue - initialValue) * m_progress;

        m_implSeq->dmxData[(universe - 1) * 512 + (channel-1)] = currentValue;

    }

}

//时间结束, 停止计算单元的定时器

if (m_progress >= 1.0) 

{

    m_fadeTimer->stop();

//原子整数类型标志位减1

    int b = m_implSeq->updateTimerFlag.fetchAndSubRelaxed(1);

    if(b == 1)

    {

//只有当updateTimerFlag标志位值为0的时候(即没有计算单元在执行),updateTimer停止.

        m_implSeq->updateTimer.stop();

    }

    startOutDelay();

}

}

void Cue::startOutDelay()

{

emit sendFlagSubSignal();  //触发数据发送结束信号

QTimer::singleShot(cueAttr.outDelay * 1000, this, [this]() {

    emit cueFinished();

});

}

总结说明
多个计算单元的并发执行, 计算量较小, 通过定时器+事件循环实现, 更新定时器 和 发送计时器 需要在 还有计算单元执行的周期内, 保持运行状态, 触发 数据更新和数据发送 函数, 所以这里采用原子整数类型累加标志位updateTimerFlag和sendTimerFlag(示例中未展示) 来管理定时器的打开和关闭, 确保更新和发送的正常工作的同时, 当标志位为0时,停止无效的数据更新以及数据流的发送. 节省资源, 优化性能.