将存储在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芯片。
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
};
据网上多数开发者经验,以及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)。
默认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
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,实际应用中如果速率太大则需要在下位机添加缓存设计。
如图所示,可以正确的接收到下位机传输数据并在上位机显示。
总结:
项目中上位机中对于数据帧的处理可在任何数据传输的模型中适用,主要的思想是将一整个数据帧写成一个结构体,在最后一个结构体使用int data[0]。将数据存储在指针类型中,强制转换成结构体,则数据会按照顺序填充结构体的值。根据这个结构写出的数据超时检测以及数据完整性检测也值得借鉴。
关于上位机接收数据的多线程处理框架也适用于多数场合,数据接收线程连接接口FIFO和用户自定义共享内存块,为防止阻塞和卡顿中间设置一个过渡缓冲区;数据处理线程不断从共享内存中读取数据,在处理过程中按照需求设立缓存大小和个数,最后这两个子线程和主线程通过信号和槽的方式交互。
第三,这个小项目的框架是一个完整的数据流框架,在多数项目中可以此项目为蓝本进行修改和优化以完成某些模块的验证工作;另一方面对于刚学习的人来说,这也是一个很好的练手的项目。在此过程中注意对于数据传输速度,接口带宽和存储容量的分析即可。