硬件实时系统和通用软件系统之间的交互永远是让工程师头大的问题。前面在文章《QUdpSocket 丢包测试与解决》中,我们探讨了使用UDP协议的数据吞吐。
UDP/IP是一种较为高层的协议。USRP的SDR驱动libuhd也使用了类似的协议。实际应用中发现,UDP协议在低功耗CPU上,还是会出现丢包的问题。这周协助一位老师,调试他的SDR板子。这个板子类似USRP N210,但是使用了万兆网。由于带宽达到了AD9361的极限,也就是61.44MHz,在16位IQ模式下,持续数据速率高达245.76MBps,也就是近2Gbps.
与USRP N210不同,这个板子的驱动做的不咋地,提供了配置接口,数据直接用UDP发到目的IP,需要用户的程序自己接收。用户反馈,数据包老是不连续。换了一台AMD工作站,解决了问题,但他的上位机形态是小型化的机顶盒样式,CPU就那么几种可选,都是低功耗的。
抱着侥幸心理,想先评估一下FPGA传输到网卡上的数据是不是连续的,于是用wireshark静默(非滚屏)抓取,发现wireshark抓的不错。偶尔有整片的丢包,提高wireshark高级配置里相应网卡的缓存,就很连续了。
既然wireshark静默保存(不滚动,不显示,只存盘)不丢包,那理论上说明CPU是来得及的。问题还是出在上位机的程序上。上位机的软件是用Qt UDP Socket做的,切换到windows API不香吗? 不过受到wireshark的启发,我们考虑,能不能抛弃UDP,直接在以太网层面传输数据呢?
说干就干,详细了解nPCAP这个开源软件后,被这个库强大的功能吸引了。官网直接下载安装包和SDK,还有很多例子。本文就基于官网的例子来修改。
nPCAP是WPCAP的后继,在windows 10 下工作是正常的。修改FPGA的逻辑,省去了复杂的UDP封装逻辑,直接变为以太网包,反而更简单了。
以太网头部就14字节,开启网卡的巨帧模式后,9014字节中,9000字节可以传输数据,效率更高了。为了不和正常的IP协议冲突,我们自定义一个协议编号,比如0x0909,来表示包。包的有效数据长度为8400字节。
目的地址 | 源地址 | 包类型 | 数据段 | CRC |
---|---|---|---|---|
6字节 | 6字节 | 2字节,0x0909 | 8400字节 | 4字节 |
不过一般抓的数据中,尾部是没有CRC的。FPGA修改后,已经直接在wireshark里看到了包。
更方便的是,还可以指定抓取的条件,直接过滤包:
ether proto 0x0909
这个过滤规则非常灵活,参考这个链接:
https://www.tcpdump.org/manpages/pcap-filter.7.html
npcap或者Linux的libpcap代码是完全一样的。直接按照例子改一改就行了。
我们通过枚举接口,可以获得各个网卡的详细配置。
#include "pcapio.h"
#include
#include
namespace PCAPIO{
std::string ifaddresses(pcap_if_t *d);
std::string pcapio_interfaces(std::map<std::string,std::string> & devmap)
{
std::string res;
pcap_if_t *alldevs;
char errbuf[PCAP_ERRBUF_SIZE];
if(pcap_findalldevs(&alldevs, errbuf) == -1){
res = errbuf;
return res;
}
for(auto d = alldevs; d != NULL; d = d->next){
std::string d_name = d->name;
std::string d_des = ifaddresses(d);
devmap[d_name] = d_des;
}
pcap_freealldevs(alldevs);
return res;
}
std::string address_print(unsigned char * v)
{
std::string res;
char buf[1024];
res += "HEX(";
for (unsigned char i =0; i<sizeof(sockaddr::sa_data);++i)
{
if (i) res += ":";
snprintf(buf,1024,"%02X", (unsigned int)v[i]);
res += buf;
}
res += ")";
res += "DEC(";
for (unsigned char i =0; i<sizeof(sockaddr::sa_data);++i)
{
if (i) res += ":";
snprintf(buf,1024,"%03u", (unsigned int)v[i] );
res += buf;
}
res += ")";
return res;
}
/* Print all the available information on the given interface */
std::string ifaddresses(pcap_if_t *d)
{
char buf[1024];
pcap_addr_t *a;
std::string res = "NAME=";
res+=d->name;
res += "\n";
if(d->description)
{
res += "Description=";
res += d->description;
res += "\n";
}
for(a=d->addresses;a;a=a->next) {
snprintf(buf,1024," AF_0x%02X:",(unsigned int)a->addr->sa_family);
res += buf;
if (a->addr)
{
snprintf(buf,1024,"\n Addr:%s ",address_print((unsigned char *)a->addr->sa_data).c_str());
res += buf;
}
if (a->netmask)
{
snprintf(buf,1024,"\n Mask:%s ",address_print((unsigned char *)a->netmask->sa_data).c_str());
res += buf;
}
if (a->broadaddr)
{
snprintf(buf,1024,"\n Cast:%s ",address_print((unsigned char *)a->broadaddr->sa_data).c_str());
res += buf;
}
if (a->dstaddr)
{
snprintf(buf,1024,"\n Dest:%s ",address_print((unsigned char *)a->dstaddr->sa_data).c_str());
res += buf;
}
res += "\n";
}
return res;
}
}
主函数里进行调用,并让用户选择:
#include
#include
#include "pcapio.h"
#include
using namespace std;
int main()
{
std::map<std::string,std::string > devmap;
std::vector<std::string> names;
std::string errstring = PCAPIO::pcapio_interfaces(devmap);
if (errstring.size())
{
cout <<"Error:\n"<<errstring<<"\n";
return 0;
}
for (auto & p : devmap)
{
names.push_back(p.first);
cout <<"==========\n";
cout << "Device " << names.size() <<":\n";
cout <<p.second;
}
cout <<"Input src device id 1~"<<names.size()<<":";
unsigned int nID = 0;
cin >> nID;
if (!nID || nID > devmap.size())
{
cout <<"Invalid ID out of range\n";
return 0;
}
std::string strDev = names[nID-1];
//...
}
枚举结果类似:
==========
Device 1:
NAME=\Device\NPF_Loopback
Description=Adapter for loopback traffic capture
==========
Device 2:
NAME=\Device\NPF_{EE5F07CD-9A1B-4494-941B-BCD0B5CC1138}
Description=VirtualBox Host-Only Ethernet Adapter
AF_0x17:
Addr:HEX(00:00:00:00:00:00:FE:80:00:00:00:00:00:00)DEC(000:000:000:000:000:000:254:128:000:000:000:000:000:000)
Mask:HEX(00:00:00:00:00:00:FF:FF:FF:FF:FF:FF:FF:FF)DEC(000:000:000:000:000:000:255:255:255:255:255:255:255:255)
Cast:HEX(00:00:00:00:00:00:FE:80:00:00:00:00:00:00)DEC(000:000:000:000:000:000:254:128:000:000:000:000:000:000)
AF_0x02:
Addr:HEX(00:00:C0:A8:38:01:00:00:00:00:00:00:00:00)DEC(000:000:192:168:056:001:000:000:000:000:000:000:000:000)
Mask:HEX(00:00:FF:FF:FF:00:00:00:00:00:00:00:00:00)DEC(000:000:255:255:255:000:000:000:000:000:000:000:000:000)
Cast:HEX(00:00:C0:A8:38:FF:00:00:00:00:00:00:00:00)DEC(000:000:192:168:056:255:000:000:000:000:000:000:000:000)
Input src device id 1~2:
用户只要输入整数,就能选取特定的网卡。
PCAP有一个函数叫做pcap_set_buffer_size(); 用于设置缓存大小。当数据速率很高时,建议设置到128MB。同时,最厉害的是这个pcap库还有发送的功能,可以产生任意的包直接丢给网卡。这个功能可以用于高速转发。
#include
#include
#include "pcapio.h"
#include
using namespace std;
int main()
{
//接上文
//std::string strDev = names[nID-1];
pcap_t *handle = NULL;
char errbuf[PCAP_ERRBUF_SIZE];
handle = pcap_open_live(strDev.c_str(), 65535, 1, 10, errbuf);
if(handle == NULL)
{
printf("pcap_open_live return err,errbuf:%s...\n", errbuf);
return -1;
}
struct bpf_program filter;
char filter_app[] = "ip"; //替换为ether proto 0x0909
bpf_u_int32 net = 0;
int ret32 = pcap_compile(handle, &filter, filter_app, 0, net);
if(ret32 < 0)
{
printf("pcap_compile return %d, errbuf:%s\n", ret32, errbuf);
return -1;
}
ret32 = pcap_setfilter(handle, &filter);
if(ret32 < 0)
{
printf("pcap_setfilter return %d, errbuf:%s\n", ret32, errbuf);
return -1;
}
const u_char *packet;
struct pcap_pkthdr header;
//注意,要设置较大的缓存
pcap_set_buffer_size(handle,256*1024*1024);
while (1)
{
packet = pcap_next(handle, &header);
if(packet)
{
printf("LEN=%d:", header.len);
for (unsigned int i=0;i<header.len;++i)
{
if ((i>=14 && i<32) || i+4 >= header.len)
printf ("%02X",packet[i]);
else if (i==32)
printf ("...");
}
printf ("\n");
//如果需要转发,则要打开另一个handle。
//pcap_sendpacket(handle_dst,packet,header.len);
}
}
pcap_close(handle);
return 0;
}
在实际的上位机程序里,采用一个独立的线程抓包,并设置环装缓存器以及一个读写原子(atomic)指针进行并发操作,测试效果很好。环装缓存的原理类似我的博文《环状缓存器与lambda代码块内线程在USRP SDR实时吞吐中的应用》所述,这里不再赘述。此外,在Linux下,API接口是完全一致的。相关程序由好友上传到git仓库。
这种情况适用于端到端的直接连接,或者是只经过交换机,没有经过路由器的直接连接。所有通过IP地址进行包路由的通道都是无法适用的。