Overview
Pcap2HDL is a Verilator bench for early network-silicon bring-up.
A Wireshark or tcpdump .pcap is read in C with libpcap,
streamed through DPI-C, and driven onto an AXI-Stream so HDL sees the
same Ethernet bytes a MAC would, with simulation time frozen.
Default DATA_W is 8 (one byte per cycle);
make AXIS_W=64 packs eight bytes with tkeep.
A beat is legal only when tvalid && tready.
Default make uses bench BP as tready.
make NIC=1 lets nic_rx drive ready (with BP
as ready_mask). Template diagrams:
Bring your own NIC.
The figure is the whole design. Top to bottom is capture → C → testbench → bus.
pkt_size_filter, pkt_header_parser,
pkt_roce_icrc, and pkt_ip_csum are not a chain:
they snoop the same AXIS in the same cycles.
Only pkt_header_parser fans down to pkt_tcp_tracker,
pkt_roce_tracker, and pkt_rss.
pkt_roce_icrc stays on AXIS and also takes
is_roce and ip_tot_len from pkt_header_parser.
pkt_size_filter and pkt_ip_csum have no downward
arrows; they never feed the trackers.
pkt_size_filter, pkt_header_parser,
pkt_roce_icrc, and pkt_ip_csum.
Parser metadata from pkt_header_parser only.
pkt_roce_icrc also samples parser is_roce / ip_tot_len.
Optional DUMP= writes accepted beats back to a pcap.
Capture and DPI-C
The top of the figure is the only path that touches the filesystem.
make PCAP=traffic.pcap plusargs into tb_pcap_dpi.sv.
Imports are implemented in dpi/pcap_reader.c.
BPF skip happens here: skipped frames never become beats.
C also computes RSS and IPv4 checksum for the scoreboard
(get_rss_hash, get_ip_csum).
Optional DUMP= writes accepted beats back
(dump_put_byte / dump_packet) so Wireshark can open the round-trip.
| Import | Role |
|---|---|
| open_pcap | Open capture; fail the sim if missing |
| set_pcap_filter | Compile BPF; skip is counted, not silent |
| fetch_next_packet | Next matched frame length, or 0 at EOF |
| get_packet_byte | One captured byte for the current frame |
| get_wire_len / get_ts_* / get_datalink | Wire length, timestamps, DLT |
| get_rss_* / get_ip_csum_* | C golden for HDL scoreboard |
| open_pcap_dump / dump_put_byte / dump_packet | Round-trip pcap of accepted beats |
Skipped BPF frames never raise tvalid. Truncated captures
(captured length < wire length) still stream; ICRC may skip rather than fail.
AXI-Stream
The wide bar in the figure is the only packet interface.
For each matched frame the testbench puts bytes on the bus.
Clock is 10 ns (always #5 clk = ~clk). Reset is active-low.
There is no FIFO between DUT modules; the same wires go to
pkt_size_filter, pkt_header_parser,
pkt_roce_icrc, and pkt_ip_csum.
tuser is the C RSS hash and tuser_err is C checksum-fail;
pkt_rss and pkt_ip_csum do not read them.
Beat
A beat is one AXI-Stream handshake: one clock where
tvalid and tready are both 1. That is the only
cycle the DUT may take tdata / tkeep.
In the HDL this is fire (tvalid && tready).
DATA_W=8 → one beat is one byte.
make AXIS_W=64 → one beat is up to eight bytes;
tkeep marks which lanes are valid.
tstart is the first beat of the frame, tlast the last.
If tready is 0 (BP=1 / BP=2), there is
no beat that cycle even if tvalid is 1.
A 64-byte frame is 64 beats at 8 bits, or 8 beats at 64 bits
(the last beat may have a short tkeep).
The testbench drives tdata/tkeep on negedge,
sets tvalid, then waits posedge until tready.
| Signal | Width | Direction (DUT) | Meaning |
|---|---|---|---|
| clk / rst_n | 1 | in | Synchronous design; async assert of reset in the TB |
| tdata | DATA_W | in | Payload; little-endian lanes when DATA_W=64 |
| tkeep | DATA_W/8 | in | Valid bytes in the beat; all 1s except a short last beat |
| tvalid | 1 | in | Master has a beat |
| tready | 1 | out* | From nic_rx (ready_mask is bench BP) |
| tstart | 1 | in | First beat of the frame (not in AMBA AXIS; explicit here) |
| tlast | 1 | in | Last beat of the frame |
| tuser | 32 | in | C RSS hash when valid; for a custom NIC DUT |
| tuser_err | 1 | in | C checksum fail; does not overwrite tuser |
*Observers input tready. The in-tree nic_rx outputs
s_tready, which already includes bench BP as
ready_mask. Confirm fire in the wave before trusting parse.
Bring your own NIC
Hierarchy, AXIS pin template, and make NIC=1 are on the
Bring your own NIC page. hdl/nic_rx.sv is the
in-tree slave; observers stay under u_snoop.
Without the flag the log prints [NIC] off.
Gate with the slave compiled:
[NIC] rx= equals streamed packets,
drop=0, byte_mis=0, mis=0.
Streaming DUTs
pkt_size_filter, pkt_header_parser,
pkt_roce_icrc, and pkt_ip_csum all sample
tvalid && tready. They are observers, not a chain.
pkt_size_filter and pkt_ip_csum stop on AXIS.
Only pkt_header_parser continues downward.
pkt_roce_icrc stays on AXIS and also takes parser
is_roce and ip_tot_len.
| Module | On AXIS | Produces |
|---|---|---|
| pkt_size_filter | tkeep popcount | pkt_done, pkt_bytes, runt / standard / jumbo |
| pkt_header_parser | byte index through Ethernet / IP / L4 / BTH | hdr_valid and header fields |
| pkt_ip_csum | own IHL walk | csum_valid, csum, ok / err / skip vs C |
| pkt_roce_icrc | same beats + is_roce, ip_tot_len | icrc_valid, ICRC ok / err / skip |
C vs HDL
DPI-C and SystemVerilog both fold the IPv4 header checksum (RFC 1071).
They do not share an implementation: C runs in dpi/pcap_reader.c
when the frame is fetched; pkt_ip_csum walks AXIS beats.
tb_pcap_dpi latches the C result, then on csum_valid
compares csum and csum_ok to
get_ip_csum / get_ip_csum_ok.
A mismatch increments n_csum_mis and prints
[CSUM] … MISMATCH. The gate is mis=0.
get_rss_hash in C versus
pkt_rss on parser metadata; the testbench prints
[RSS] and counts mis.
Parser metadata
NIC pipelines call this parser metadata (P4: a packet header
vector). pkt_tcp_tracker, pkt_roce_tracker, and
pkt_rss hang only off pkt_header_parser.
They never see tdata.
When hdr_valid is high for a cycle, the fields below are that
metadata. The testbench compares HDL RSS and checksum to C
(mis=0).
| Group | Signals |
|---|---|
| Classify | is_ipv4, is_non_ipv4, is_truncated, is_tcp, is_udp, is_roce, is_arp, is_vxlan |
| L2 | dst_mac, src_mac, ethertype (0480, 0806, …) |
| IPv4 | src_ip, dst_ip, ip_ttl, ip_tot_len, ip_proto, is_len_mismatch |
| L4 | src_port, dst_port; TCP seq, ack, flags, plen |
| RoCE BTH | opcode, pkey, ackreq, dest_qp, psn |
| VXLAN | vxlan_vni, inner type via the same L2/L3 walk |
| Consumer | Uses |
|---|---|
| pkt_tcp_tracker | hdr_valid, is_tcp, 4-tuple, seq, ack, flags, plen |
| pkt_roce_tracker | hdr_valid, is_roce, IPs, opcode, dest_qp, psn |
| pkt_rss | hdr_valid, is_ipv4, truncated, proto, 4-tuple |
| pkt_roce_icrc | AXIS plus is_roce, ip_tot_len (not hdr_valid alone) |
| tb scoreboard | rss_hash vs get_rss_hash; csum vs get_ip_csum |
One packet
Walk the figure once around the clock. DPI returns length and bytes; C RSS
and checksum are latched. Beats run on AXIS from tstart to
tlast; pkt_size_filter, pkt_header_parser,
pkt_roce_icrc, and pkt_ip_csum update every fire.
When the header is complete (often before tlast on a long frame),
hdr_valid pulses and pkt_tcp_tracker,
pkt_roce_tracker, and pkt_rss fire.
On tlast, pkt_size_filter pulses pkt_done
and pkt_roce_icrc checks the last four bytes if RoCE and not truncated.
Then tvalid drops; PACE may insert IFG from pcap
timestamps (capped by PACE_MAX_US), plus three idle cycles by default.
In the wave, start with tvalid / tready /
tstart / tlast / tdata on one packet.
Then hdr_valid and ethertype, then
tcp_evt or trk_evt. If AXIS is wrong, parser and
trackers will lie even if the .pcap is fine.
Run a sim, then make wave (GTKWave on simulation_trace.vcd).
Logs: [HDR], [TCP], [TRK],
[RSS], [CSUM], [DUT].
Sources
| Path | Role |
|---|---|
| hdl/tb_pcap_dpi.sv | DPI imports, AXIS master, plusargs, logs |
| hdl/pkt_snoop.sv | Observer hierarchy under u_snoop |
| hdl/pkt_size_filter.sv | Runt / standard / jumbo from tkeep popcount |
| hdl/pkt_header_parser.sv | Streaming L2–L4, ARP, VXLAN, Soft-RoCEv2 BTH |
| hdl/pkt_tcp_tracker.sv | Handshake and next-seq CAM |
| hdl/pkt_roce_tracker.sv | PSN / message / reverse ACK |
| hdl/pkt_roce_icrc.sv | Masked CRC32 vs last 4 bytes |
| hdl/pkt_rss.sv | Toeplitz; same key as C |
| hdl/pkt_ip_csum.sv | IPv4 header checksum; same fold as C |
| hdl/nic_rx.sv | AXIS slave; compiled only with make NIC=1 |
| dpi/pcap_reader.c | libpcap, BPF, dump, C RSS and csum |