Replay Wireshark or tcpdump Ethernet PCAP files into cycle-accurate SystemVerilog HDL in Verilator.
Pcap2HDL streams .pcap files into Verilator through DPI-C (libpcap). Each captured byte is driven on an AXI-Stream-like bus (tdata, tkeep, tvalid, tready, tstart, tlast) so hardware under test can see the same frames a NIC would, with simulation time frozen for debug. Default width is 8 bits (one byte per cycle); make AXIS_W=64 packs eight bytes per beat. DUT modules accept a beat only when tvalid && tready. This is offline pcap replay, not live sniff, and not GL Communications proprietary .HDL file conversion.
Typical uses: early bring-up of FPGA or ASIC packet pipelines (classification, DPI, RoCE-aware paths) in Verilog/SystemVerilog before silicon or a live Ethernet port is available.
Current release: 0.6.0. Project site: khademullah.github.io/Pcap2HDL. Architecture (DPI-C, AXI-Stream, parser metadata): architecture. Bring your own NIC: nic. Demo (16:9): pcap2hdl-demo.mp4.
Linux (Ubuntu is the reference environment), Verilator 5.032 or later, and:
sudo apt update
sudo apt install build-essential libpcap-devGTKWave is optional (make wave). Soft-RoCE capture additionally needs rdma-core, ibverbs-utils, and tcpdump; see docs/soft_roce_veth.md.
| Path | Role |
|---|---|
Makefile |
Compile, run, waveforms, clean |
hdl/ |
SystemVerilog testbench and DUT |
hdl/tb_pcap_dpi.sv |
DPI imports, AXI-Stream master, plusargs |
hdl/pkt_snoop.sv |
Observer hierarchy (filter, parser, trackers, ICRC, RSS, csum) |
hdl/nic_rx.sv |
Optional AXIS slave; compiled only with make NIC=1 |
hdl/pkt_size_filter.sv |
Frame length: runt / standard / jumbo |
hdl/pkt_header_parser.sv |
L2–L4 parse; TCP; Soft-RoCE BTH |
hdl/pkt_roce_tracker.sv |
RoCE PSN / MSG_DONE / ACK_OK |
hdl/pkt_tcp_tracker.sv |
TCP handshake and next-seq |
hdl/pkt_roce_icrc.sv |
RoCEv2 ICRC |
hdl/pkt_rss.sv |
NIC RSS Toeplitz; compared with DPI-C |
hdl/pkt_ip_csum.sv |
IPv4 header checksum; compared with DPI-C |
dpi/pcap_reader.c |
libpcap DPI-C: read, BPF, dump, RSS, IPv4 csum |
traffic.pcap |
Local iperf TCP trace (not in git) |
soft_roce.pcap |
Local Soft-RoCEv2 trace from scripts/soft_roce_veth.sh |
docs/ |
Capture notes, stills, GitHub Pages (index.html) |
examples/ |
Reference simulation logs |
scripts/soft_roce_veth.sh |
veth + RXE + ibv_rc_pingpong capture helper |
scripts/gen_pcap.py |
Tiny ARP/TCP/VXLAN/runt/IPv6/802.1Q IPv4+IPv6 pcap (stdlib) |
.github/workflows/ci.yml |
Generate ci.pcap and make BP=2 |
.pcap -> libpcap -> DPI-C -> tb_pcap_dpi (AXIS master)
├─ pkt_snoop [DUT observers]
│ u_filter / u_parser / u_tcp / u_tracker
│ u_icrc / u_rss / u_csum
└─ u_nic_rx [only if make NIC=1]
drives tready
dump.pcap <- libpcap (optional)
open_pcap()opens the file named by+PCAP=;get_datalink()reports the capture DLT. Optional+FILTER=compiles a libpcap BPF program; DPI-C appliespcap_offline_filterper frame so HDL only sees matches and the bench reports how many frames were skipped. Optional+DUMP=writes accepted AXI-Stream bytes back throughpcap_dump.- Packets are replayed up to
+MAX_PACKETS=(default 100). After eachfetch_next_packet(),get_wire_len()/get_ts_sec()/get_ts_usec()expose the pcap header (on-wire length vs storedcaplen, capture timestamp). - Bytes are updated on the clock negedge and sampled on posedge when
tvalid && tready. Defaultmakeuses benchBPastready.make NIC=1compilesnic_rx: it drivess_tready(credits, optionalNIC_PAUSE) ANDready_mask(BP). WithAXIS_W=64, up to eight bytes share a beat. The master holdstvaliduntil the handshake. pkt_size_filtercountstkeepbits and pulsespkt_donewith length class.pkt_header_parserlatches MAC, EtherType, optional 802.1Q (0x8100, VID, L3 at 18), IPv4 (TTL, total length), L4 ports, TCP sequence/flags, RoCE BTH, ARP (0x0806), and VXLAN (UDP/4789, inner Ethernet, VNI).pkt_roce_trackerfollows Send First/Middle/Last PSN per{src,dst,qp}and matches the reverse-direction ACK.pkt_tcp_trackerfollows SYN / SYN-ACK / ACK (HS_DONE) and then next expected seq from TCP payload length.pkt_roce_icrcchecks the last 4 bytes of a complete RoCEv2 frame (masked CRC32). Truncated captures are skipped.pkt_rsscomputes Microsoft Toeplitz RSS on the IPv4 4-tuple (queue = hash % 4). DPI-C hashes the same bytes intotuser;mismust stay 0.pkt_ip_csumfolds the IPv4 header (RFC 1071) and compares with DPI-C (CSUM mis=0).tuser_errflags a C-side checksum fail without changing the RSS hash.make NIC=1instantiatesu_nic_rxundertb_pcap_dpi. Without the flag,nic_rx.svis not compiled and the log prints[NIC] off.
make # observers only; [NIC] off
make NIC=1 # compile nic_rx; [NIC] rx=… mis=0
make NIC=1 BP=2
make NIC=1 NIC_PAUSE=1 # slave toggles s_tready
make PCAP=soft_roce.pcap # Soft-RoCEv2
make PCAP=soft_roce.pcap MAX_PACKETS=16
make PACE=1 # IFG from pcap timestamps (capped at 100 us)
make BP=1 # tready low every other cycle
make BP=2 # extra random tready AND-ed with the slave
make NIC_PAUSE=1 # slave toggles its own tready (BP=0 still)
python3 scripts/gen_pcap.py ci.pcap
make PCAP=ci.pcap MAX_PACKETS=8 BP=2
make NIC=1 PCAP=ns1_iperf6.pcap MAX_PACKETS=20 # local IPv6 iperf; not in git
make AXIS_W=64 # 8-byte AXI-Stream beats
make DUMP=replay.pcap # write the bus back to a pcap
make FILTER='tcp port 5201' # libpcap BPF; HDL only sees matches
make FILTER='udp' # no UDP in traffic.pcap; streamed 0
make PCAP=soft_roce.pcap FILTER='udp port 4791'
make wave # GTKWave on simulation_trace.vcd
make cleanTraces are selected at runtime; a rebuild is not required when only PCAP or MAX_PACKETS changes. Changing AXIS_W or NIC rebuilds.
Build with make NIC=1. hdl/nic_rx.sv is compiled and u_nic_rx is the slave. Observers live under u_snoop.
hdl/nic_rx.sv is the template. It produces s_tready.
- Copy
nic_rxor replaceu_nic_rxinhdl/tb_pcap_dpi.sv(inside`ifdef EN_NIC). - Keep this port list:
clk,rst_n,s_tdata,s_tkeep,s_tvalid,s_tready(output),s_tstart,s_tlast,s_tuser,s_tuser_err. - Sample only when
s_tvalid && s_tready.tstartis SOP (not in AMBA);tlastis EOP. - Leave
ready_maskconnected to the benchBPgenerator, or tie it to 1 if your core is the only ready source. Do not ANDBPonly outside the slave while still sampling on unmaskeds_tready— that double-counts beats. - Keep
make NIC=1(or add your file next tonic_rx.svin theifeq ($(NIC),1)block). - Gate:
[NIC] rx=equals streamed packets,drop=0,byte_mis=0,mis=0.
NIC_PAUSE=1 (with NIC=1) is a slave-side stall inside nic_rx: each clock toggles bubble, so s_tready is 0 every other cycle even if the FIFO is not full (s_tready = rst_n && ready_mask && !bubble && !almost_full). That is not BP: BP=1/BP=2 is bench ready_mask. Default NIC_PAUSE=0 leaves bubble at 0. Packet counts and [NIC] mis=0 should still match NIC_PAUSE=0 if the slave only advances on s_tvalid && s_tready. Your RX can tie pause_en off. Details: nic.html.
This is an ingress example (credits + frame length), not a 400GbE MAC. Diagrams: docs/nic.html.
makeOne [HDR] line is printed when headers are valid (after L4 ports for TCP/UDP). One [DUT] line follows tlast. DPI reports DLT, stored vs on-wire length, and the pcap timestamp. MAC swap with a stable 192.168.1.1 / 192.168.1.2 pair is a two-host iperf conversation.
[SV] Datalink DLT=1 (Ethernet)
[SV] Processing Packet #1 (captured 74 / wire 74 bytes) ts=1790231670.733870
[CSUM] ffff OK
[HDR] ... IPv4 ttl=64 iplen=60 192.168.1.1:34612 -> 192.168.1.2:5201 TCP SYN seq=0x492bad66 ack=0x00000000 plen=0
[RSS] q=0 hash=3eb75670
[TCP] SYN_OK
[DUT] Classified packet: 74 bytes -> STANDARD
[SV] Processing Packet #2 ...
[HDR] ... IPv4 ttl=64 iplen=60 192.168.1.2:5201 -> 192.168.1.1:34612 TCP SYN ACK seq=0x6a9f5792 ack=0x492bad67 plen=0
[TCP] SYNACK_OK
...
[TCP] HS_DONE
...
[TCP] SEQ_OK
...
[DUT] Header ipv4=8 ipv6=0 vlan=0 tcp=8 udp=0 roce=0 arp=0 vxlan=0 other=0 trunc=0
[DUT] TCP hs=1 fin=0 rst=0 seq_ok=5 seq_err=0 op_err=0
[DUT] Length mismatch=0
[DUT] CSUM ok=8 err=0 skip=0 mis=0
[DUT] RSS q0=5 q1=0 q2=3 q3=0 mis=0 skip=0
Full run: examples/traffic_8pkt.log.
Waveform: docs/gtkwave_8pkt.jpg. Each tvalid burst is one frame (tstart / tlast). At millisecond zoom the 10 ns clock looks solid; zoom into a burst to see edges.
make DUMP=replay.pcap
make PCAP=replay.pcapDUMP writes accepted AXI-Stream bytes (tvalid && tready) back through libpcap. Replaying that file must match the original DUT summary (hs=1 seq_ok=5 seq_err=0). Wireshark opens replay.pcap as Ethernet: the same eight frames (SYN, SYN-ACK, ACK, then iperf PSH/ACK, 103-byte cookie on packet 4). Still: docs/wireshark_replay.png.
libpcap compiles the expression (pcap_compile). DPI-C does not call pcap_setfilter; each frame goes through pcap_offline_filter so skipped packets are counted. The DUT is unchanged: it only ever sees frames that pass BPF. MAX_PACKETS counts matches, not raw file order. Skip is frames walked over while filling that cap (unread tail of the file is neither match nor skip).
make FILTER='tcp port 5201'[C-DPI] BPF filter: tcp port 5201
[SV] Streaming up to 100 packets FILTER=tcp port 5201
[HDR] ... 192.168.1.1:34612 -> 192.168.1.2:5201 TCP SYN ... plen=0
[TCP] SYN_OK
...
[C-DPI] BPF matched=100 skipped=0
A miss still opens the file; pcap_next() returns nothing for HDL:
make FILTER='udp'[C-DPI] BPF filter: udp
[SV] Reached end of PCAP before hitting the packet cap.
[C-DPI] BPF matched=0 skipped=1000
[SV] Simulation finished. File=traffic.pcap Streamed 0 packets.
[DUT] Header ipv4=0 ipv6=0 tcp=0 udp=0 roce=0 arp=0 vxlan=0 other=0 trunc=0
Full run: examples/bpf_udp.log.
Soft-RoCE is UDP/4791, so the same knob keeps the RoCE DUT and drops TCP:
make PCAP=soft_roce.pcap FILTER='udp port 4791'[HDR] ... IPv4 ttl=64 iplen=1068 192.168.10.1:49441 -> 192.168.10.2:4791 ROCE SEND_LAST qp=0x11 psn=0xe1b96f pkey=0xffff AckReq
[TRK] MSG_DONE
[DUT] ICRC 8a401571 OK
[HDR] ... ROCE ACK ...
[TRK] ACK_OK
[DUT] ICRC dabe7a49 OK
[DUT] Header ipv4=8 ipv6=0 vlan=0 tcp=0 udp=0 roce=8 arp=0 vxlan=0 other=0 trunc=0
[DUT] Tracker msg=1 ack=1 psn_err=0 op_err=0 psn_gap=0
[DUT] ICRC ok=8 err=0 skip=0
[DUT] CSUM ok=8 err=0 skip=0 mis=0
A 1000-packet veth capture can stay one file: make FILTER='tcp port 5201' streams 100 matches (skipped=0 if the head of the file is all TCP); make FILTER='udp' streams 0 and reports skipped=1000.
make PCAP=soft_roce.pcap
make PCAP=soft_roce.pcap MAX_PACKETS=16
make PCAP=soft_roce.pcap FILTER='udp port 4791'UDP/4791 frames are tagged ROCE with BTH opcode, dest QP, and PSN. The tracker emits OK on in-order fragments, MSG_DONE on Send Last, and ACK_OK on the matching reverse ACK. An 8-packet cap finishes mid-message in the reverse direction (msg=1 ack=1). MAX_PACKETS=16 completes three messages (msg=3 ack=3, three runt ACKs) and starts the next Send. Compact logs: examples/README.md (traffic_8pkt.log, soft_roce_8pkt.log, soft_roce_16pkt.log, ci_nic.log, ipv6_20pkt.log, bpf_udp.log).
[HDR] ... IPv4 ttl=64 iplen=1068 192.168.10.1:49441 -> 192.168.10.2:4791 ROCE SEND_LAST qp=0x11 psn=0xe1b96f pkey=0xffff AckReq
[TRK] MSG_DONE
[HDR] ... IPv4 ttl=64 iplen=48 192.168.10.2:49441 -> 192.168.10.1:4791 ROCE ACK qp=0x11 psn=0xe1b96f pkey=0xffff
[TRK] ACK_OK
[DUT] Classified packet: 62 bytes -> RUNT
[DUT] ICRC dabe7a49 OK
...
[DUT] Header ipv4=8 ipv6=0 vlan=0 tcp=0 udp=0 roce=8 arp=0 vxlan=0 other=0 trunc=0
[DUT] Tracker msg=1 ack=1 psn_err=0 op_err=0 psn_gap=0
[DUT] Length mismatch=0
[DUT] ICRC ok=8 err=0 skip=0
[DUT] CSUM ok=8 err=0 skip=0 mis=0
[DUT] RSS q0=0 q1=8 q2=0 q3=0 mis=0 skip=0
Full run: examples/soft_roce_8pkt.log. Sixteen packets: examples/soft_roce_16pkt.log.
Capture a new file with sudo ./scripts/soft_roce_veth.sh setup then demo (docs/soft_roce_veth.md).
Local capture (not in git). Base header only.
make NIC=1 PCAP=ns1_iperf6.pcap MAX_PACKETS=20[HDR] ... etype=0x86dd IPv6 hop=255 iplen=56 fe80…:0 -> ff02…:0
[HDR] ... IPv6 hop=64 iplen=80 fd00…:44432 -> fd00…:5201 TCP SYN seq=0x6f2e64f4 ...
[TCP] SYN_OK
...
[TCP] HS_DONE
...
[DUT] Header ipv4=0 ipv6=20 vlan=0 tcp=17 udp=0 roce=0 arp=0 vxlan=0 other=0 trunc=0
[DUT] TCP hs=2 fin=0 rst=0 seq_ok=11 seq_err=0 op_err=0
[DUT] CSUM ok=0 err=0 skip=20 mis=0
[DUT] RSS ... mis=0 skip=0
[NIC] rx=20 drop=0 byte_mis=0 ... mis=0
Packets 1–3 are ICMPv6 (multicast, hop 255). Two iperf TCP sessions (44432 and 44438). Full run: examples/ipv6_20pkt.log. CI synthetic SYN: examples/ci_nic.log.
- Environment: DPI-C pcap stream into Verilator (bytes, wire length, timestamp, DLT)
- Control: packet cap and clean exit
- Size filter: runt / standard / jumbo (
tkeeppopcount) - L2/L3: MAC, EtherType, one 802.1Q tag (
0x8100, VID, L3 at 18); IPv4 TTL and total length (LEN_MISMATCH vs captured); IPv6 base header (hop limit, payload+40 as iplen; no extension headers) - IPv4 checksum: RFC 1071 in C and HDL (
CSUM mis=0), including after a VLAN tag;tuser_erris fail sideband. IPv6 has no header checksum (skip). - L4: TCP/UDP ports; TCP flags, seq, ack
- RoCEv2 BTH: opcode, dest QP, PSN, P_Key, AckReq
- RoCEv2 ICRC: last 4 bytes checked; truncated captures skipped
- Tracker: RoCE PSN/ACK and next-message PSN_GAP; TCP SYN / SYN-ACK / HS_DONE / next-seq (
plen) / FIN / RST - RSS: Toeplitz 4-tuple in C and HDL (
tuser); four queues,mis=0(IPv6 32/36-byte key input) - ARP (
0x0806) and VXLAN (UDP/4789, VNI, inner Ethernet) - Replay: optional
+PACE=1IFG from pcap timestamps (capped);AXIS_W=64eight-byte beats; slavetreadyfromnic_rxAND+BP=1/+BP=2;+NIC_PAUSE=1stalls inside the slave;DUMP=writes the bus back to a pcap;FILTER=is libpcap BPF (pcap_offline_filter) withmatched/skippedcounts - Bring your own NIC: optional
nic_rxAXIS slave (make NIC=1); observers underu_snoop - Coverage print
[COV]size / TCP flags / RoCE opcodes. CI:.github/workflows/ci.yml+scripts/gen_pcap.py
Still parked: QinQ, IPv6 extension headers. See CHANGELOG.md. How to contribute: CONTRIBUTING.md.
See LICENSE.
