Real and I/Q
Channels is about how many channels a converter has. This page is about what a sample on one of them is — real or complex — which is answered in two different places, and the relationship between them is the thing on this page worth reading twice.
Complex-ness is a property of the word
An I/Q design does not get twice as many ports. It gets a wider sample:
real = Rfsoc4x2SampWord.specialize(samp_per_word=4) # 4 real samples, 64 bits
iq = Rfsoc4x2SampWord.specialize(samp_per_word=2, iq_mode=True) # 2 complex samples, 64 bits
Both are 64-bit words on the same bus; an I/Q design fits by halving samp_per_word, because a
complex sample occupies two slots. The port count never moves.
That is a deliberate modelling choice, and the alternative is worth naming. The IP’s wiring for an
I/Q datapath can be two streams — on a dual-tile (Gen 1) part, I and Q come out of m00_axis and
m01_axis separately. Mirroring that into the model would make every consumer downstream — the
buffers, the BFM, your logic — learn a port-pairing rule in order to say “complex”. Carrying
complex-ness as a type keeps it a data question, which is where it belongs. On the quad-tile
parts this project targets (ZU48DR / RFSoC 4x2) the hardware interleaves I and Q on one bus anyway,
so the model’s port is bit-identical to the IP’s.
The RF environment is complex baseband
Every signal on the RF side is modelled in complex baseband, including one that is physically a real passband waveform: a real passband signal is its complex envelope, and the envelope is what the model carries.
This is the observation that removes a whole subsystem. The RFDC’s digital up- and down-converter — NCO, complex mixer, the thing that turns I/Q into a real IF — becomes, in this representation:
(a, b) -> a + ib
Forming a complex number. Not multiplying by a carrier. No NCO, no complex multiply, and no equivalence obligation — there is no arithmetic here that a C++ twin would have to be proven bit-exact against.
Two consequences follow, and the second is the useful one:
- I/Q → real and I/Q → I/Q are indistinguishable from the RF side. Both present one complex baseband channel. Whether the quadrature mixing happens on-chip or in an external analog modulator is a fact about the analog domain past the model’s boundary.
- Where the conversion happens becomes a modelling choice, and the model has to be able to say which. That is what the second flag is for.
The two flags
| lives on | asks | |
|---|---|---|
word.iq_mode |
the sample word | bus packing — does one beat carry interleaved I/Q? |
RFSampIF.complex_samp |
the RF edge | signal representation — does this edge carry a complex baseband envelope? |
They are genuinely different questions, and three of their four combinations are legal:
iq_mode |
complex_samp |
what it means |
|---|---|---|
0 |
0 |
real baseband end to end — direct sampling, no conversion anywhere. Every example in this repo today. |
0 |
1 |
the DUC/DDC is in the RF domain, outside the converter. The edge is complex-typed because the environment uniformly is; its content is real. |
1 |
1 |
the DUC/DDC is in the converter — interleaved I/Q beats, complex blocks. |
1 |
0 |
refused. The beats carry a Q and the edge has nowhere to put it. |
The rule is an implication, not an equality:
word.iq_mode ⇒ complex_samp
and the reason is one sentence: the converter performs no I/Q mapping, so it can never create a Q. A complex word needs a complex edge. A real word is fine on either.
The (0, 1) row, and the guard on it
This is the row that needs a moment. A real converter on a complex-baseband edge is what you have
whenever the down-conversion ran upstream of the ADC, or the up-conversion runs downstream of the
DAC. The edge is complex-typed because the RF environment is uniformly complex baseband; the content
is real, x + j0, because the conversion happened elsewhere. Taking the real part is then exact.
Rfdc.rf_samples() does that, and checks the “then” rather than assuming it:
if np.any(arr.imag):
raise ValueError(... "carries a non-zero Q, but the AXIS word is real" ...)
A live Q on such an edge is not a representation detail. A real converter cannot carry it, and dropping it would hand the fabric a block of the right shape and length holding half a signal — which is exactly the class of failure the RF path is built to make impossible, arriving by a side door.
Two real channels into one complex signal
A design that drives an external quadrature modulator from two real DAC channels needs
(a, b) → a + ib across two channels, which halves the channel count. That belongs to a separate
pysim-only RF block, outside the converter — the same discipline that keeps gain and multipath in
Channel rather than in the edge. It keeps n_ch = n_rx = n_tx on the converter and its two sides
symmetric.
That block does not exist yet. Nothing has needed it; it is a handful of lines when something does.
What the converter checks, and when
At bind, Rfdc reads what it does not own and refuses what cannot work:
- the RF edge’s
n_chmust equaln_rx/n_tx— see channels; word.iq_mode ⇒ complex_samp, above.
That second check is the same rule RFSampIF.put() already
applies one level down — it refuses a complex block on a real edge and widens a real block on a
complex one. An equality check here would be stricter than the interface it guards.
At pre_sim, once the clocks are bound: samp_rate <= samp_per_word · f_axis, which is the
port’s capacity and not your design’s — see rule 4.
Next
- The sample word — how
iq_modesits beside slot order, justification andiq_order. - The RF side — where
complex_sampis declared, and the widening rule. - Fidelity — what this model does not tell you.
Source of truth: waveflow/hw/rfdc.py, waveflow/hw/rf_sample_if.py,
waveflow/hw/rfdc_samp_word.py, plans/adc_model.md § Channels, ports, and where I/Q lives.