Rfdc

Waveflow’s RF converter is a basic model for a DAC and ADC converter interfacing to logic via an AXI stream. The model follows AMD’s RF Data Converter IP. The AMD RFDC also provides configurable digital up- and down-conversion — NCO frequency, mixer settings, interpolation and decimation factors. Waveflow does not model that configuration capability today; over time, we will emulate it. Carrying I/Q samples is a separate question and is supported — see real and I/Q.

Interfaces

In Waveflow, the python class for the converter is Rfdc and represents one module carrying both directions, with a different kind of port on each side:

Rfdc between your logic and the RF environment: rx_rf and tx_rf carry sample blocks on the RF side, rx_streams[0] and tx_streams[0] carry packed words on the fabric side

side endpoints carries clock
RF rx_rf, tx_rfRFSampIF blocks of samples samp_clk, up to GSa/s
fabric rx_streams[i], tx_streams[i]StreamIF, one per channel words on AXI-Stream axis_clk, a few hundred MHz

Two facts fall out of that picture, and they set the order of everything below.

The two sides run at rates an order of magnitude apart. A converter samples far faster than the logic behind it, so it cannot hand over one sample per fabric cycle. The AXI-Stream carries several samples packed into each beat — which means that before you can instantiate an Rfdc at all, you have to say how many, and how they are arranged. That is what the first page is for.

Only the stream endpoints cross the cut. The RF endpoints have no RTL counterpart — they are the boundary whose far side we refuse to model at RTL. That asymmetry is why the two sides read so differently in the pages that follow.

Pages

The order is do → understand → limits:

The design rules are late, but read them early if you are already building. They are assembled from the pages before them, so they read best once you have seen where each came from — but rules 1–4 make a design correct and 5–7 make it checkable, and a rule broken at design time costs more than one read out of order.


Table of contents

  • Multi-channel support - Configuring how many receive and transmit datapaths a converter has, and what a channel costs on each of its two sides: one AXI-Stream port per datapath on the fabric side, matching AMD's channelization, and one RFSampIF per direction on the RF side carrying every channel of that direction in a single block. Also what you would have to do by hand to lower a multi-channel design onto real RFDC blocks, and why the word tile does not mean here what it means in AMD's documentation.
  • Real and I/Q - What a sample on a channel is. Complex-ness is a property of the word rather than of the port structure, so an I/Q design gets a wider sample and not twice as many ports. The RF environment is complex baseband throughout, which turns the RFDC's digital up/down-converter from a piece of DSP into a choice of representation — and which is why the converter carries two independent flags whose relationship is an implication rather than an equality.
  • Adding an RF path - What to write when a design needs an RF conversion path. The complete wiring for a receive path in one block — one Rfdc, one RFSampIF on the sample side, one ordinary StreamIF on the fabric side — then the three numbers you have to decide, the word type that carries the rest, the width that is derived for you, and what you can do with the AXI-Stream that comes out: consume it directly, or put a sample buffer behind it.
  • The sample word - Why a converter hands the fabric several samples at a time — the clock ratio makes it unavoidable — and the convention that says which sample lands where. Slot order, the difference between the bits a converter resolves and the bits its slot occupies, where those effective bits sit inside the container, and which of I and Q comes first. All of it is one type, RfdcSampWord, because the rules are AMD's rather than Waveflow's and belong somewhere a reader can see them.
  • Instantiating the converter - How to create an Rfdc: the word type that carries its sample geometry, the full parameter list and which kind each parameter is, the four endpoints and which two cross the cut, what the constructor refuses and why, and the bit-exact quantization the model does. The how-to page — the contracts it implies are in the design rules.
  • Connecting the RF side - Wiring the converter's sample side: one RFSampIF per direction, what each of its parameters means, the file-backed source and sink that stand in for the RF environment, and t0 — pushed by the converter, read back as the thing that makes alignment a derived assertion rather than a scheduling coincidence.
  • Connecting the fabric side - Wiring the converter to your logic: an ordinary AXI-Stream at the word type's width, why there is no samples-per-cycle parameter but two derived and generally fractional ratios instead, the rate check the converter performs at pre_sim and the larger one it cannot perform for you, and why a depth declared on a boundary port is silently discarded.
  • Block sampling - The model underneath the wiring pages: one SimPy event carries one (n_ch, blksize) block of samples, the block duration is the timing, and NumPy is the function. Covers blksize as the fidelity/speed knob; why the metronome lives in the interface rather than in a node; why it schedules on an absolute grid and a relative timeout loop demonstrably slips; t0 and the sample grid; and the underrun/overrun asymmetry the counters exist to record.
  • The design rules - Seven rules for a design that talks to a converter. Rules 1-4 make a design correct: never stall the ingress, two tasks are not automatically overlap, the buffer has to exist somewhere, and the port's capacity is not your design's. Rules 5-7 make it checkable: the counters are the contract, timestamps come from the sample index, and only an internal channel's depth is real. Each is stated with the one measurement that paid for it.
  • The fidelity boundary - What block-level modelling can and cannot tell you. The contract has three conditions — behaviour depends only on sample timestamps, no dependency shorter than two blocks, and the DUT never stalls its input — and only the third is mechanically checkable. It now is, in pysim, as `dropped == 0`. The page also says plainly where that check stops seeing: a consumer that stalls inside a block period is below the model's resolution, demonstrated by a design where RTL loses 72 words and pysim reports none.