FPGA1—ROM存储经千兆以太网口到Qt上位机显示2022-10-23

1.场景:

将存储在FPGA片上BlockRAM中的图片数据通过网口传输到上位机显示目标是FPGA通过网口发送图片,其大小为1920*1200,位深为8bit,30fps,上位机可以实时显示即可。这个小项目中考虑的问题①使用FPGA的Block Memory Generate IP,②开发图片数据转换成coe文件的软件,①②两步实现将图片数据存储到FPGA片上。③通过千兆以太网口将ROM图片数据传输到上位机。④使用Qt开发网口数据接收上位机,先可以显示一张图片,然后通过多线程配置使之能够动态显示图片流即视频数据。该项目主要用来验证,网口传输到上位机多线程显示两个问题,ROM图片数据可以看成是模拟的视频流,实际应用可以来自其他数据源如CMOS图像传感器等。大的验证步骤分为两个,一个是验证静态图片即一张图片的数据流,在此基础上验证视频流传输与显示。该小项目开发环境基于Qt5.15.2,vivado2020.2,winsock2.h ,以及xc7a35tfgg芯片。

2.实现思路:

  1. 确认xc7a系列的片上RAM只有1.8Mbit,按照python2000最终预想设置,分辨率为1920*1200,位深是8bit,因此一张图片的大小为1920*1200*8bit=17.5Mbit,17.5/12=1.46Mbit<1.8Mbit,因此只能保存1/12图像,重复发送12次便模拟一张图片的全部像素。(截图取自xilinx 7系列选型手册)

    FPGA1—ROM存储经千兆以太网口到Qt上位机显示2022-10-23_第1张图片

  2. 使用qt制作coe文件生成程序,生成coe文件。先用画图板构造一个1920*1200(注意像素生成错了为1900*1200费了好大的劲才找见)大小的图片,在qt程序中,只写前面1920*100个像素数据到coe文件中(据缓存大小决定)参考C5—Qt生成特定应用的COE文件 2021-11-18修改部分代码即可,我在此基础上修改了图像像素大小,并且将最后一行的逗号改为分号。如下图所示。

    FPGA1—ROM存储经千兆以太网口到Qt上位机显示2022-10-23_第2张图片

  3. 创建FPGA工程(为验证单张图片的工程)。程序概述:上电后,等待上位机发出ARP请求(因此上位机程序应主动发出ARP请求,此处也是程序需要完善的地方,只能应答ARP而不能发出ARP请求),当收到ARP请求,可在PC端通过arp - a查看arp映射表的情况,按键按下一次只发送一张图片(即发送12次ROM数据),让上位机接收,以验证单张图像的显示。此处关于网口的描述可参考E1--千兆以太网接口测试应用2022-09-07
  4. 使用qt制作网口数据显示程序,发现网上和Qt官方有一直言论Qt5对于UDP Socket的支持不完善,果断采用Windows对于网络编程的支持,即socket编程。对于socket编程的简介请看下节。
  5. 关于发送格式的确定。添加从ROM到网口的数据,每一张图片都12部分相同的数据组成,每一次传输(一个UDP数据包)1232B数据包,其中包含32B帧头信息和1200B图片信息,一张图片由1920个图像帧传输完成;帧头信息采用以下结构体。(上述3,4部分均要遵循)
    struct  FrameHeader
    {
    quint32 flag; //固定为 FLAG_HEAD 对数据合法性判断,防止其他数据干扰帧头
    quint32 width; //图片宽度 1920
    quint32 height; //图片高度 1200
    quint32 total; //一张图片总大小 例如 1920*1200Bytes
    quint32 offset; //当前数据偏移量 例如,第一帧数据为 0, 第 n 帧数据为 (n-1)*framesize
    quint32 picseq; //图片序号,第几张图片
    quint32 frameseq; //一张图片发送的帧序号,当前图片的第 n 帧 ,从 1~ 1920
    quint32 framesize; //当前帧图片数据大小 例如每一次都发有效图片数据大小为 1200
    };
    
  6. 开始测试,明确下位机关于配置,端口为6000,IP地址为192.168.1.10,Mac地址为0x00-11-22-33-44-55,本地端口为6001,应该设置本地IP为同一个网段,即手动ip设为192.168.1.102。(使用arp -a命令查看ARP地址映射表)
  7. FPGA1—ROM存储经千兆以太网口到Qt上位机显示2022-10-23_第3张图片

    查看结果如下,单张图片显示正常,以下两图分别为原图和复原图。(注单张图片显示的上下位机代码未贴出,只贴出最后动态显示的代码)FPGA1—ROM存储经千兆以太网口到Qt上位机显示2022-10-23_第4张图片FPGA1—ROM存储经千兆以太网口到Qt上位机显示2022-10-23_第5张图片
  8. 下面考虑发送视频流时数据的恢复,基于单张显示成功的FPGA工程创建V2 FPGA工程,控制每30fps速度发送ROM至网口;开发上位机程序,划分为主线程、接收线程、处理线程三个线程。主要开发的代码是网口数据接收、数据解码、图片恢复、多线程控制等内容。主要考虑的问题是线程同步和线程安全。即缓存大小、类型、位置以及共享内存信号量和互斥量的设计。其数据流向简要如下:

FPGA1—ROM存储经千兆以太网口到Qt上位机显示2022-10-23_第6张图片

3.Win Socket编程

据网上多数开发者经验,以及Qt creator书,Qt5版本的Network模块并不完善,在使用UdpSocket套接字接受数据时,无论是否使用线程去接收,常常出现丢包的现象。索性直接使用win socket编程,其实就是用微软官方提供的关于网口编程的库winsock2.h。

环境搭建

添加头文件#include "Winsock2.h",并且在.pro文件中添加LIBS += -lWS2_32,即可使用windows网络编程。

流程

与Qt提供的UDP Socket相比,获取网口收发数据的流程大同小异,socket()-->bind( )-->listen()-->accept()-->read()/write()--->close()。关于UDP Socket可参考Day30Qt实现UDP传输2022-01-07,关于socket的教程网上很多,下面我把用到的几个函数罗列如下。

①初始化 DLL

WSADATA wsaData;//WSADATA数据类型:这个结构被用来存储 被WSAStartup函数调用后返回的 Windows Sockets数据
WSAStartup( MAKEWORD(2, 2), &wsaData);// WSAStartup就是为了向操作系统说明,我们要用哪个库文件,让该库文件与当前的应用程序绑定,从而就可以调用该版本的socket的各种函数了

②创建套接字

原型:SOCKET socket(int af, int type, int protocol);

    af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址  type 为数据传输方式/套接字类型,流格式套接字(Stream Sockets)(TCP套接字)数据报格式套接字(SOCK_DGRAM)(UDP套接字)protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议 如果使用 SOCK_DGRAM 传输方式,且位ipv4,那么满足这两个条件的协议只有 UDP返回值是 SOCKET 类型,用来表示一个套接字。
③结构体sockaddr_in

struct sockaddr_in {
       short   sin_family;         //地址族
       u_short      sin_port;      //16bit TCP/UDP端口号
       struct in_addr   sin_addr;  //32位IP地址
       char    sin_zero[8];        //不使用
       };

④将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)。htonl函数可以用来进行网络字节和主机字节的转换。

uint16_t htons(uint16_t hostshort);

⑤bind()函数:

    int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
    //sockfd, 表示使用bind函数的套接字描述符(即socket函数的返回值)
    //my_addr, 指向sockaddr结构(用于表示所分配地址)的指针
    //addrlen, 用socklen_t字段指定了sockaddr结构的长度
    //如果发生错误,函数返回值为-1,否则为0

⑥INADDR_ANY

    作为接收端,当你调用bind()函数绑定IP时使用【INADDR_ANY】,表明接收来自任意IP、任意网卡的发给指定端口的数据。作为发送端,当你调用bind()函数绑定IP时使用【INADDR_ANY】,表明使用网卡号最低的网卡进行发送数据,也就是UDP数据广播。

⑦setsockopt   

    int setsockopt(SOCKET s, int level, int optname, const char FAR *optval, int optlen);
    //s,标识一个套接口的描述字;
    //level,被设置的选项的级别, 目前仅支持SOL_SOCKET和IPPROTO_TCP层次,想要套接字级别上设置选//项,就必须把level设置为 SOL_SOCKET
    //optname,需设置的选项
    //optval,指向存放选项值的缓冲区
    //optlen 缓冲区的长度
    //若无错误发生,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误

⑧recvfrom函数

    int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
    //s,socket描述符。
    //buf,UDP数据报缓存地址。
    //len,UDP数据报长度。
    //flags,该参数一般为0。
    //sockaddr,recvfrom()函数参数,struct sockaddr_in类型,指明从哪里接收UDP数据报
    //fromlen,对方地址长度,一般为:sizeof(struct sockaddr_in)。

4.上位机代码分析

默认main.cpp文件

#include "widget.h"
#include 
#include 
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

主线程widget文件-widget.h

该文件主要是创建数据接收线程类对象和数据处理线程类对象,声明必要的函数和变量,具体功能在cpp文件中描述。

#ifndef WIDGET_H
#define WIDGET_H
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "recvthread.h"
#include "handlethread.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
private slots:
    //udp recv thread slot
    void serverStartedSlot(bool bok);
    //handle thread slot
    void showPixmapSlot(QPixmap pix);
    void slotSpeedOut();
    void on_startBtn_clicked();
    void on_stopBtn_clicked();
    void recvThdMessage(QString);
    void on_clearBtn_clicked();

private:
    Ui::Widget *ui;
    QUdpSocket *udpSocket;//udp管理对象
    recvThread* recvthread;
    HandleThread* handlethread;
    int nPicCount;
    QTimer timerSpeed;
    void Init();
    void appendMessage(QString);
};
#endif // WIDGET_H

主线程widget文件-widget.cpp

widget.cpp主要的功能是①通过UDP Socket套接字发送一段数据,数据内容本身不重要,重要的是发送一段数据会先产生一个ARP请求,由于我的下位机只能ARP应答暂未实现主动ARP请求,因此通过这样的方式获取下位机的mac地址,维护相应ARP映射表。②管理子线程的启动与停止③子线程执行后,通过槽相应子线程的信号,完成图片数据的接收与显示,帧率的计算与显示;④设置状态栏显示打印相关状态信息。

#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{

    ui->setupUi(this);
    Init();
}

Widget::~Widget()
{
    delete ui;

    recvthread->bwork = false;
    recvthread->quit();
    recvthread->wait();
    delete recvthread;
    handlethread->bwork = false;
    handlethread->quit();
    handlethread->wait();
    delete handlethread;
}

//由于下位机缺陷,需要上位机发起一次传输请求,才能进行ARP映射
void Widget::on_startBtn_clicked()
{
    QString msg = "努力给自己的年少轻狂一个交代!";
    QString peer_ip = ui->ip_Ledit->text();
    QString peer_port = QString("6000");
    if(udpSocket>writeDatagram(msg.toUtf8(),QHostAddress(peer_ip),peer_port.toUInt())>0)        
    {
        appendMessage("成功1!");
        ui->startBtn->setEnabled(true);
        ui->stopBtn->setEnabled(true);
    }
    else{
        appendMessage("失败1!");
    }
    recvthread->strIP = ui->ip_Ledit->text();
    if(recvthread->strIP.isEmpty()){
        recvthread->strIP = "127.0.0.1";
    }
    recvthread->start();
    nPicCount = 0;
    timerSpeed.start(1000);
}
void Widget::on_stopBtn_clicked()
{
    recvthread->bwork = false;
    recvthread->quit();
    timerSpeed.stop();
}
void Widget::recvThdMessage(QString str)
{
    QString strmsg = QString("[%2] %1").arg(str)
            .arg(QTime::currentTime().toString("hh:mm:ss"));
    //打印提示信息
    ui->textBrowser->append(strmsg);
}
void Widget::serverStartedSlot(bool bok)
{
    QString msg;
    if(bok){
        msg = "开始接收....";
    }else{
        msg = "停止接收....";
    }
    appendMessage(msg);
}
void Widget::showPixmapSlot(QPixmap pix)
{
    nPicCount++;
    QPixmap scalpix = pix.scaled(pix.width()/2,pix.height()/2) ;
//        ui->showLabel->resize(ui->showLabel->width(),ui->showLabel->height());
//    qDebug()<<"接收图片的宽为:"<showLabel->setPixmap(scalpix);
}
void Widget::slotSpeedOut()
{
    ui->rateLabel->setText(QString::number(nPicCount));
    qDebug()<<"打印帧率判断"<setWindowTitle(tr("JC UDP Camera(V1.00)"));

    connect(recvthread,SIGNAL(serverStarted(bool)),this, SLOT(serverStartedSlot(bool)));
    connect(handlethread,SIGNAL(showPixmapSig(QPixmap)),this, SLOT(showPixmapSlot(QPixmap)));
//    connect(handlethread,SIGNAL(showPixmapSig(QPixmap)),
//            this, SLOT(showPixmapSlot(QPixmap)),Qt::BlockingQueuedConnection);
    connect(handlethread, SIGNAL(showMsgSig(QString)),this, SLOT(recvThdMessage(QString)));
    handlethread->start();
    connect(&timerSpeed, SIGNAL(timeout()),this, SLOT(slotSpeedOut()));
}

void Widget::appendMessage(QString str)
{
    //添加时间戳
    QString strmsg = QString("[%2] %1").arg(str)
            .arg(QTime::currentTime().toString("hh:mm:ss"));
    //打印提示信息
    ui->textBrowser->append(strmsg);
}

void Widget::on_clearBtn_clicked()
{
    ui->textBrowser->clear();
}

数据接收子线程--recvthread.h

该文件主要定义了一个与主线程交互的信号serverStarted,作了一些必要声明。

#ifndef RECVTHREAD_H
#define RECVTHREAD_H
#include 
#include 
#include "WinSock2.h"
#include 
#include 
//#include "proto.h"
#include "dataqueue.h"
#include "frame_def.h"
class recvThread : public QThread
{
    Q_OBJECT
public:
    explicit recvThread(QObject *parent = nullptr);
    ~recvThread();
    bool bwork;
    QString strIP;
protected:
    void run();
signals:
    void serverStarted(bool bok);
public slots:
};
#endif // RECVTHREAD_H

数据接收子线程--recvthread.cpp

该文件定义了数据接收线程的功能,run函数按照win socket的流程实现了网口数据的接收,并将这些数据通过缓冲存入到共享内存中。

#include "recvthread.h"
//LIBS += -lWS2_32  网络编程要在pro里面添加这句话
recvThread::recvThread(QObject *parent)
    : QThread{parent}
{
      WSADATA wsaData;
      WSAStartup( MAKEWORD(2, 2), &wsaData);
}
recvThread::~recvThread()
{
    WSACleanup();

}
void recvThread::run()
{
    bwork = true;
    //1.创建套接字
    SOCKET servSock = socket(AF_INET, SOCK_DGRAM, 0);
    //2.bind绑定套接字
    struct sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr)) ;  //每个字节都用0填充
    sockAddr.sin_family = AF_INET;  //使用IPv4地址
    sockAddr.sin_port = htons(6001);  //本地端口
    if(bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR))< 0){
         emit serverStarted(false);
    }
    else{
        emit serverStarted(true);
        sockaddr_in clientAddr;
        int clientAddrSize = sizeof (clientAddr);
        qDebug()<< " ip:" << strIP;
        // 设置超时时间
        timeval timeout;
        timeout.tv_sec = 0;
        timeout.tv_usec = 0;
        char recv_buf[1480] = {0};
        fd_set rfd;
        int nRecvBuf = 30 * 1024 * 1024;   
        int nRet = setsockopt(servSock,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
        qDebug() << "set sock ope ret:" << nRet;
        int nFramePicsize = 1200;
        while(bwork){
            FD_ZERO(&rfd);
            FD_SET(servSock, &rfd);
            nRet = select(0, &rfd, NULL, NULL, &timeout);
            if(0 == nRet)//超时
            {continue;}
            else if(-1 == nRet){
                qDebug() << "socket read error!";
                break;
            }else{
                if(FD_ISSET(servSock, &rfd)){//有数据读取
                    int nrecv = recvfrom(servSock, recv_buf, sizeof(recv_buf),0,(SOCKADDR *)&clientAddr, &clientAddrSize);
                    QString straddr = QHostAddress(htonl(clientAddr.sin_addr.S_un.S_addr)).toString();
                    if(straddr == strIP){
                        if((sizeof(stHeader)+nFramePicsize) == nrecv)
                        {
                            char* buf = (char*)malloc(nrecv);
                            if(!buf){
                                qDebug() << "malloc memory failed";
                                continue;
                            }
                            memcpy(buf, recv_buf, nrecv);
                            dataqueue::getInstance()->inQueue(buf);
                        }
                        else{
                            qDebug() << "datagram size invalid:" << nrecv-sizeof(stHeader);
                        }
                    }
                }
            }
    }
    closesocket(servSock);
    emit serverStarted(false);

}
}

共享内存dataqueue.h

#ifndef DATAQUEUE_H
#define DATAQUEUE_H
#include 
#include 
#include 
class dataqueue : public QObject
{
public:
    static dataqueue* getInstance();
    void inQueue(char* buf);
    void outQueue(QList &buflist);
private:
    explicit dataqueue(QObject *parent = nullptr);
    ~dataqueue();
    QMutex* mutex;
    QList cache;
signals:
public slots:
};
#endif // BUFQUEUE_H

共享内存dataqueue.cpp

使用Qt容器类创建了一块共享内存,提供了写入和读出的方法,通过Qmutex对该内存提供了互斥量保护。

#include "dataqueue.h"
static dataqueue* g_buf = 0;
dataqueue *dataqueue::getInstance()
{
    if(!g_buf){
        g_buf = new dataqueue;
    }
    return g_buf;
}
void dataqueue::inQueue(char *buf)
{
    QMutexLocker locker(mutex);
    cache.push_back(buf);
}
void dataqueue::outQueue(QList &buflist)
{
    QMutexLocker locker(mutex);
    buflist = cache;
    cache.clear();
}
dataqueue::dataqueue(QObject *parent) : QObject(parent)
{
    mutex = new QMutex(QMutex::Recursive);
}
dataqueue::~dataqueue()
{
    delete mutex;
}

数据处理子线程--handlethread.h

#ifndef HANDLETHREAD_H
#define HANDLETHREAD_H

#include 
#include 
#include 
#include 
#include "frame_def.h"
class HandleThread : public QThread
{
    Q_OBJECT
public:
    explicit HandleThread(QObject *parent = nullptr);

    ~HandleThread();

    bool bwork;
protected:

    void run();
    char *picData;
    int ntotalFrames;//一帧图片帧数目
    QMap picCacheMap;//完整一张图片的缓存map
    QMap >frameseqMap;//一张图片所有帧序号的缓存map,用以判断是否完整和丢了多少帧
    QMap picHeartMap;//一张图片最后帧数据的接收时间戳。用来判断超时丢弃

signals:
    void showPixmapSig(QPixmap pix);

    void showMsgSig(QString tips);

public slots:
};

#endif // HANDLETHREAD_H

数据处理子线程--handlethread.cpp

从共享内存中拿出数据,构造成Qpixmap对象,将完整帧然后通过信号的方式发送给主线程。在数据重组过程中进行超时检测和完整性检测。

#include "handlethread.h"
#include "dataqueue.h"
#include 
#include 
#include 
#include 
#include 

HandleThread::HandleThread(QObject *parent) : QThread(parent)
{
    ntotalFrames = 1920;
}

HandleThread::~HandleThread()
{

}

void HandleThread::run()
{

    qDebug() << "handle thread begin to parse datagram" << Q_FUNC_INFO;
    bwork = true;
    picData = new char[sizeof(stHeader)+1920*1200];//这个缓冲区用于构造一个图片帧
    while(bwork){
         //第一部分:使用outQueue从缓冲区获取一帧(1232B)数据解析
        QList buflist;
        dataqueue::getInstance()->outQueue(buflist);
        //第二部分:使用for循环,保存图片的一张完整数据、帧序号list、最后帧的时间戳,只按照数据源解析然后分门别类的保存,如果第一部未获取到数据,该部分也不会执行
        //解析图片数据 只要buflist.count()!=0的时候这段代码才会执行
        //for循环阶段,只对数据做分组拼接保存操作,不做任何判断,不产生任何结果,不发出任何信号
        //在for循环结束后,数据的完整性以及是否超时就可以判断了,此时三个容器中保存着或完整的一帧数据
        for(int i = 0; i < buflist.count(); i++){//帧长为1232
            //1.获取图片帧1232长 32(B)帧头+1200(B)图像有效数据
            char* by_1F_Temp = buflist.at(i); //获取一帧数据 1200个长
            stHeader* F1_st = (stHeader*)by_1F_Temp;//一帧数据直接格式化成结构体处理
            //2.保存图片的有效数据,有效数据保存在
            if(picCacheMap.contains(F1_st->picseq)){//如果当前图片的数据空间已经创建
                memcpy(picData+sizeof(stHeader)+F1_st->offset, F1_st->data, F1_st->framesize);
                picCacheMap[F1_st->picseq] = picData;
            }
            else{//创建当前图片的地址空间
//                char* by_1P_Temp = (char*)calloc(sizeof(stHeader)+F1_st->total,sizeof(char));//空间大小为32+1920*1200
                stHeader* st_1Pic_Temp = (stHeader *)picData;//强制一张图片的数据格式转化为  header+data
                //为图片帧头赋值
                st_1Pic_Temp->flag = F1_st->flag; //0x55aa66bb
                st_1Pic_Temp->width = F1_st->width;//1920
                st_1Pic_Temp->height = F1_st->height;//1200
                st_1Pic_Temp->total = F1_st->total;//1920*1200
                st_1Pic_Temp->picseq = F1_st->picseq;//图片序号,从0到32字节写满,然后在往回调
                st_1Pic_Temp->framesize = F1_st->framesize;//当前帧有效像素大小 1200
                //存储第0帧数据

                memcpy(picData+sizeof(stHeader)+F1_st->offset, F1_st->data, F1_st->framesize);
                picCacheMap[F1_st->picseq] = picData;
            }
            //3.保存时间戳 不管是那一帧,只要是这一帧的就立马更新时间戳,不影响任何操作
            quint32 LastTime = QDateTime::currentDateTime().toSecsSinceEpoch();
            picHeartMap[F1_st->picseq] = LastTime;
            //4.保存帧序号list
            QList frameseqlist =frameseqMap.value(F1_st->picseq);
            if(!frameseqlist.contains(F1_st->frameseq)){//不包含这个序号就添加
                frameseqlist.push_back(F1_st->frameseq);
                frameseqMap[F1_st->picseq] = frameseqlist;
            }
            free(by_1F_Temp);
        }


        //第三部分检查数据完整性和超时。数据完整就发送信号到主线程,不完整就超时检查,超时即丢弃

        QMapIterator > iter(frameseqMap);//迭代器遍历帧序号容器,判断数据是否完整
        while(iter.hasNext()){
            iter.next();
            if(ntotalFrames == iter.value().count()){//够数了
//                qDebug() <width,
                             phead->height,
                             QImage::Format_Grayscale8);
                QPixmap pixmap = QPixmap::fromImage(image);
                emit showPixmapSig(pixmap);
//                free(pdata);//??free函数释放ptr参数指向的内存空间。该内存空间是由malloc、calloc或realloc函数申请的。否则,该函数将导致未定义行为,如果ptr参数是NULL,则不执行任何操作。
             //果然在这里出错了!!不可以这样做!栈区内存由系统统一管理
                picCacheMap.remove(iter.key());
                frameseqMap.remove(iter.key());
                picHeartMap.remove(iter.key());
            }

            else{
                //不够数的时候就要时刻判断是否超时
                quint32 cursecs = QDateTime::currentDateTime().toSecsSinceEpoch();
                //检查数据完整性和发送超时
                QMapIterator > iter(frameseqMap);
                while(iter.hasNext()){//可以继续迭代
                    iter.next();
                    //超时处理
                    if(cursecs - picHeartMap.value(iter.key()) > 3){//此时iter.key表示当前图片序号
                        //如果这张图三秒前
                        //超时,移除
                        //QMap picCacheMap;//完整一张图片的缓存map
                        //键值代表图片序号,值代表各图片的完整数据,思路非常清晰!
                        qDebug()<<"get there!";
                        char* pdata = picCacheMap.value(iter.key());
                        QString tips = QString("picseq:%1 lost %2 frames").arg(iter.key())
                                .arg(ntotalFrames-frameseqMap.value(iter.key()).count());//一张图片的帧数减去现有的帧数即为丢失的
                        emit showMsgSig(tips);
//                        free(pdata);//??free函数释放ptr参数指向的内存空间。该内存空间是由malloc、calloc或realloc函数申请的。否则,该函数将导致未定义行为,如果ptr参数是NULL,则不执行任何操作。
                        picCacheMap.remove(iter.key());//删除该张图片的所有信息
                        frameseqMap.remove(iter.key());
                        picHeartMap.remove(iter.key());
                    }
                }
                msleep(30);//Forces the current thread to sleep for msecs milliseconds
                continue;
            }
        }
    }
}

frame_def.h文件定义帧数据结构体信息

#ifndef FRAME_DEF_H
#define FRAME_DEF_H
#include 
#define FLAG_HEAD   0xAA0055FF
#pragma pack(4)
struct stHeader
{
    quint32 flag;       //固定为  FLAG_HEAD 对数据合法性判断,防止其他数据干扰
    quint32 width;      //图片宽度  例如 1920
    quint32 height;     //图片高度  例如 1200
    quint32 total;      //一张图片总大小   例如  1920*1200
    quint32 offset;     //当前数据偏移量   例如,第一帧数据为0, 第n帧数据为 (n-1)*framesize
    quint32 picseq;     //图片序号,第几张图片
    quint32 frameseq;   //一张图片发送的帧序号,当前图片的第n帧 ,从0-1919
    quint32 framesize;  //当前帧图片数据大小 例如每一次都发有效图片数据大小为1200
    quint8 data[0];     //有效图片数据
};
#pragma pack()
#endif // FRAME_DEF_H

5.FPGA代码分析

FPGA工程下载链接,该篇在E1--千兆以太网接口测试应用2022-09-07的基础上修改recv模块只接收arp请求,而trans模块在收到recv的arp应答指示后发出arp应答,将有效数据替换为ROM IP出来的数据。在顶层模块添加各种帧头信息的传递,添加帧率的控制,添加两个千兆以太网帧之间的96ns间隔。需要说明的是,这里图片速度为30fps,1920*1200*8*30/s=527Mb/s  < 1000Mb*0.8,实际应用中如果速率太大则需要在下位机添加缓存设计。

6.效果与总结

如图所示,可以正确的接收到下位机传输数据并在上位机显示。

总结:

项目中上位机中对于数据帧的处理可在任何数据传输的模型中适用,主要的思想是将一整个数据帧写成一个结构体,在最后一个结构体使用int data[0]。将数据存储在指针类型中,强制转换成结构体,则数据会按照顺序填充结构体的值。根据这个结构写出的数据超时检测以及数据完整性检测也值得借鉴。

关于上位机接收数据的多线程处理框架也适用于多数场合,数据接收线程连接接口FIFO和用户自定义共享内存块,为防止阻塞和卡顿中间设置一个过渡缓冲区;数据处理线程不断从共享内存中读取数据,在处理过程中按照需求设立缓存大小和个数,最后这两个子线程和主线程通过信号和槽的方式交互。

第三,这个小项目的框架是一个完整的数据流框架,在多数项目中可以此项目为蓝本进行修改和优化以完成某些模块的验证工作;另一方面对于刚学习的人来说,这也是一个很好的练手的项目。在此过程中注意对于数据传输速度,接口带宽和存储容量的分析即可。

你可能感兴趣的:(FPGA积累——小项目,fpga开发,1024程序员节)