This web page is meant to be an introduction to how to use the RealDigital 4x2 RFSoC development
board. This work was done thanks to a collaboration between me,
Drew Baden, and
Alessandro Restelli
from JQI, and in fact Alessandro
taught me most of what I know about this subject.
This board is a "lite" version of the more powerful boards (e.g.
AMD ZCU208). The focus is on the firmware in the Xilinx part,
not on the PYNQ software, but of course
you can't use one without the other so there will be some useful information on PYNQ here.
A good place to start for a PYNQ tutorial might be
here.
An excellent online book that delves nicely into software radio can be found
here, where you can download the
latest version. Or you can grab
this one that dates from 2023.
I'm using Vivado 2024.2 here, although most of the base project that AMD supplies is made with
Vivado 2022.1. But I found 2024.2 works fine, and it's more modern.
You can find the reference manual (revision A5) for the 4x2 board
here. A schematic can be found
here.
The 4x2 has 2 DACs that run at 9.85 GSps
(actually, the max is more like 7 GSps, more on that below)
and 4 ADCs that can run at 5 GSps.
The more powerful boards have 8 DACs and 8 ADCs, different (although not necessarily better)
internal clocking, etc.
The data converters allow you to modulate a carrier wave with a lot of flexibility.
Both the RealDigital 4x2 and the ZCU208 board use the same Xilinx ZU48DR chip.
In the gen3 ZU48DR, the ADCs can convert at up to 5 GSps and
the DACs up to 9.85 GSps.
However, that chip comes in different speed grades: -1 and -2 designations,
with -2 being "faster" than -1.
What the manufacturer does is to make large numbers of chips, then test and
sort according to how fast they can push each chip. Faster ones go in the
-2 bin, slower ones in the -1 bin. And as you can imagine, they charge
more for the faster chips.
The ZCU208 uses the faster -2, but for the 4x2 it might have different
speed grades depending on what you bought it, etc. Probably most newer ones
have the -2 speed, but it turns out that it's probably impossible to know
from issuing linux commands or reading the bit or hwh files since those
reflect what was put in. But the board files determine what target you are
compiling for, and I believe those comes from RealDigital, so maybe you can
trust it. So what you can do is to find an hwh file that goes with a bit
file you've made, then in linux on the ARM chip, cd to that file location and
do this:
The main difference between the speed grades is that -2 can support higher
clock speeds for the various resources, as in the following table:
Note that UltraRam is like block ram (BRAM) in an FPGA, but it is designed
to be used for massive buffers, so it has less flexibility than BRAM but
can be cascaded and uses up less FPGA resources than BRAM. It has less
flexibility in the data worth (BRAM can be highly configurable) and not
as much capability for dual port operations, and importantly cannot be
preloaded like BRAM can. Also, a GTY transceiver is a specialized ultra fast
hardware circuit on the FPGA designed for very high speed communications.
It can be used as a serializer/deserializer (SERDES) running different
protocols (100G ethernet, PCI express), can support error correction like
Reed-Solomon, support 8b/10b, 64b/66b, and even 128b/130b, and has built-in equalization.
For our purposes, the only place where the speed grade might matter is if
we have to do massive amounts of DMA into and out of the RF Converter,
and need to push the FPGA fabric clock up to accomplish this. But this is
highly unlikely, as the 4x2 board with the -1 chip is probably
adequate for most things we will be doing.
The main difference between the 4x2 and the ZCU208 boards is that on the 4x2,
only 4 ADCs and 2 DACs are
wired up to SMA connectors, whereas on the ZCU208 all 8 ADCs and DACs are wired
up ready to use.
This is a rather versatile chip, with extensive documentation:
The architecture is outlined in this figure:
As you can see from the diagram, there are the following subsystems (which is why this is called a
System on Chip, or SOC):
The A53 processor runs PYNQ 3.1.1 ("PynqLinux"), which has a bunch of modifications for
the Zynq chip. PynqLinux is a version of linux built from Ubuntu 22.04, codenamed
"Jammy", and Ubuntu is broadly derived from Debian.
Not shown are the SMA inputs and outputs for the analog signals and external clocks. Also not
shown are 4 GBytes of 64 bit 2.4 GHz DDR4 connected to the PS, and a similar number connected to
the PL. These are the 8 rectangular chips on the board, 4 above (PL) and 4 below (PS)
the FPGA (covered
by a fan in the top image here).
The inputs and outputs and SMA clock and sync in ports are the SMA connectors on the left in
the top image, and those signals come in single ended on
SMA connectors, and are converted to differential on the inputs.
The inputs to the Digital Up Converter (DUC) on the Zynq RFSoC image above are
the modulation waveforms that you can send into the device from
the ARM chip using DMA on the AXI bus (which is a pretty simple bus).
The DACs have digital up-convertors (DUC) for taking input at some frequency and producing
an output that can change at 9.85GSps max. The ADCs have digital down-convertors (DDC) that can produce
data at any sampling rate.
The RealDigital portal to the RFSoC 4x2 is
here.
Scroll down to the bottom of that page and click on the "Resources" tab, which shows
you this:
Back to top
All Jupyter notebooks are kept on the ARM chip, so when you edit them, they reside there.
You will have to periodically backup by hand (until we find a better way). To do this:
I keep a copy on my dropbox account in case anyone needs it.
The RFSoC chip has a "processing system" part, or PS, and a "programmable logic" part, or PL.
The PL is the FPGA, and the PS consists of 2 processors:
The 32-bit R5F chip is a real-time processor, designed for low-latency tasks like real-time control
loops, interrupt handling, and fast responses to hardware events. It runs without a
general-purpose operating system which means no scheduler jitter or overhead. So you use the R5F
for real-time DAQ, hardware handshaking, etc.
Back to top
Clocking on the 4x2 is summarized in the following diagram, which you can find on page 8 of the
RFSoC 4x2 Reference Manual.
There are a great many clocks floating around on this board, but pretty much all
of these clocks start in the Skyworks Si5395
chip (SI5395B-A13886-GM). This is a clock multiplier and jitter attenuator, driven by a 48MHz crystal.
It can generate a
range of frequencies, with parameters programmed by the user stored in internal ROM. It looks like
this chip is already programmed ("factory programmed"). It has 12 clock outputs, any-frequency
synthesis architecture, jitter roughly 70-100 femtoseconds, an integrated jitter attenuation,
is I2C or SPI programmable.
The Si5395 has 2 main jobs:
The 10MHz LVDS clock output from the Si5395 is the most critical clock for the RF circuitry.
This clock that is fed
into the TI LMK04828 (LMK) chip, which also has an
input from a 160MHz
crystal oscillator made by Abracon.
The crystal oscillator has very low phase noise,
so the LMK chip phase locks it to the 10MHz reference
clock to provide a reference clock for the RF converters.
The LMK chip has 2 output clocks that go into the FPGA fabric:
The LMK chip also has 2 sets of outputs where each pair feeds a
TI LMX2594 clock synthesizer (LMX)
chips. These chips provide reference clocks for the RF converter circuitry that has the ADC and DAC components.
Of the pair of outputs, one is a 245.76MHz frequency reference used continuously by the LMX chip,
and the other is a phase alignment signal, usually a pulse that is asserted after powerup and phase
locking by the LMX chip.
To make the reference clock for the RF Converter circuitry,
each LMX chip has an internal voltage controlled oscillator (VCO) built from an LC circuit
where the capacitance derives from a diode (called a varactor) whose capacitance changes with voltage.
So suppose you want an output that is 4.9152GHz (20 times the incoming 245.76MHz)
reference clock for the RF converter
(for the ADC or DAC). (Note: the RF converter uses this reference clock to make the sampling clock that
it needs!) The software figures out all of the dividers that are needed, and programs the
LMX chip. The LMX actually has several VCO's for different ranges, so what it does is to first do a
calibration that picks the closest one, use the divider the software loaded in, divide down the VCO, and
then phase lock it to the incoming signal from the LMK to produce the right output signal.
The other magic happens in the RFConverter. So say you want to run an ADC at a frequency of
something like 3.558GHz. You would load this frequency into the RF Converter block inside
Vivado and tell it that the reference clock will be 491.52MHz. The software will figure out
how to multiply and divide that clock to get you 3.558GHz, and load those numbers into the
file to download to the RFSoC. If the rate you want isn't possible for some reason, the
software will tell you! For instance, to start with 491.52MHz and get 3.558GHz, that ratio
is 3558/491.52=7.23877. That's not an integer, but the RFSoC can still generate something
close to that
frequency by starting at 7 and adding 61/256 to get the 7.23828125, giving 3557.76MHz which
is very close using the multiplier of 491.52MHz.
Clock precision
If you look at the Si5395 data sheet, you will
see that the 48MHz oscillator has a frequency that varies by ±15ppm. This uncertainty is a due to
cutting and manufacturing uncertainties, and is constant over the lifetime of the crystal.
This means that the actual frequency can be as much as 48,000,720Hz (+15ppm) but will always have
that value every time you power up the board.
In addition to the initial ±15ppm, there are variations in the frequency
due to temperature, but these crystals are usually somewhat flat in temperature variability around
25°C. For this crystal, the temperature range of -40 to +85°C increases the frequency
uncertainty to ±25ppm. There is also a small variation of a few ppm due to aging, and even
smaller ones (load capacitance, humidity, etc). But for a board sitting in a lab at constant
temperature, one would expect a frequency due to just the initial manufacturing of ±15ppm.
That means that if you want
to use this system to generate a 1GHz tone, you can expect it to be within around 15kHz in a
controlled temperature environment.
To get the 4x2 to generate a tone with a more precise frequency, you have to use an external 10MHz precision
clock connected into the "CLK_IN" SMA input on the board. Then you can use a python Jupyter notebook
to use that clock as the reference. To do that, check out the following code. This sets up a class to overlay
to load any file you want (here it's called 'cyclic_clock.bit'), invokes the class via gen=Gen(), and tells
the system to use the external clock via gen.set_external_rf_clks(). It also sets the processor clock
to 250MHz (I think the default is 100 MHz).
Back to top
Python code
Here is an example of how to set up the clocks in the Python PYNQ environment:
Back to top
In the top image you will see a micro SD card input at the top left of the 1st 3 push buttons.
This micro SD card holds the image of the Linux OS that runs in the A53 ARM chip, implementing
the PYNQ environment.
All of the images for all of the boards can be found
here
The RealDigital 4x2 board ships with an SD card that is supposed to contain a working image.
When we bought one in summer 2024, we plugged in the micro SD card that comes with the kit,
but it didn't work at all. So we downloaded and installed the image
from the web site, version v3.0.1 (codenamed "Belfast").
This image was built with Vivado 2022.1 in mind, which means that all of the base projects
that go with the tutorials use the Vivado 2022.1 toolchain.
You can download it from the
PYNQ site.
It's a 2.26GB zip file, so
unzip, and it will create a file called "rfsoc4x2_v3.0.1.img" that will be around 10GB.
To install onto a micro SD card, best to use a Windows 11 machine. Download the app called
Win32DiskImager which you can get at a lot of sites, but I got it from
the official site.
Plug the micro SD card into the laptop somehow, it will show up as a Windows disk,
and use Win32DiskImager to write it to the SD card.
In 2025, we bought another 4x2 board and it came with an SD card, but instead of using
it, we downloaded the image from the web site, but we kept getting USB errors after bootup.
After lots of exchanges with the kind people at RealDigital, we found out that there had
been a slight change to the USB3 chip on the new versions of the 4x2, and that this change
necessitated a new version of v3.0.1. Then I found that they had actually shipped a
micro SD card that had the latest version of v3.0.1 with the USB3 fix! But they also
sent me a link to that version, which is
here. I'm not exactly sure whether v3.0.1 on the
PYNQ site has the old or new version
of v3.0.1.
Now, there's a new version on that site, v3.1.1. This version is codenamed "Carlisle",
and differs from v3.0.1 in that v3.1.1 goes with all of the project overlays that were built
with Vivado 2024.1, so you get the advantage of 2 years of AMD toolchain advances baked into
the bitstreams and kernel. V3.1.1 also fixes a bug with the transmitter gain, although
AMD didn't really document exactly what the bug was but we did find a problem with setting
the gain in software, so probably that is what has been fixed.
Also, v3.1.1 introduces a beta release of PYNQ that allows for remote access workflows, and
a new C++ integration path but I haven't looked much into these. Accessing this
new version is the same as v3.0.1 described above: download and flash onto a SD card
using Win32DiskImager.
If all goes well, after loading the card,
you can then put the SD card into the board SD slot and plug in the USB3
connector (middle right side of board) and plug the USB-A side into your computer. Plug in
the power cord (upper right side of board) and
boot it up the board by toggling the power switch.
When the board boots, wait around 30 seconds, then you should see on the LCD display
the following:
You can also ssh into the RFSoC and have the full Linux kernel capabilities. Probably
best to just use the terminal window on a MacOS machine and ssh in, or from Windows
use PowerShell.
As you can see in the photo above, the Zynq chip that contains the PS and PL is under
the fan, and there are 2 rows of 4 chips above and below, marked by a white "M".
Each of the 8 chips contains 1GB of 2.4GHz DDR4.
The row above the ZYNQ chip is used by the FPGA (the PL) and the row below is used by
the A53 ARM chip (the PS).
Each chip memory is 16-bits wide, so the number of 16-bit words is 512M per chip.
The way these chips are wired in is that all 4 chips share the same clock and command
signals, and operate in lockstep, so every memory access involves 64 bits.
But there's even more parallelism: DDR4 always works in bursts of 8 (burst length
= 8, or BL8 in the jargon). So one DDR4 read produces 8 consecutive 64 bit
transfers on the data bus, so each transfer is 64 bits, or 8 bytes, and there are 8
transfers in a burst so each BL8 produces 64B of data. And this matches the CPU cache
in the A53 processor.
We are often using the DDR4 as 32-bit memory buffers (as in some of the tutorials
here, see below). What happens when you allocate such buffers is that when you read
or write, you are always reading or writing 64 bits at a time. So for instance,
say you allocate a large 32-bit buffer and store some data in it (for example,
you store a sequential number starting at 0). Let's name the chips left to right
M1, M2, M3, M4.
Then the first memory location in your buffer will have a 1, the 2nd a 2, etc.
When you do a memory read, the computer will read 64 bits at a time, so it reads 2
buffer locations, buffer[0] and buffer[1]. The software (and the hardware) will
take care of figuring out that the lower 16 bits of buffer[0] is the first 16 bits
of M1 and the upper 16 bits of buffer[0] is the first 16 bits of M2.
Then the lower 16 bits of buffer[1] is the first 16 bits of
M3 and the upper 16 bits of buffer[1] is the first 16 bits of M4.
Reading again, it uses the 2nd 16 bits of M1 and the 2nd 16 bits of
M2 to form buffer[2], and etc.
This means that each DDR4 read, which delivers 64B in a BL8 burst, will actually
read 16 32-bit words at a time. And as you can imagine, since you are often accessing
data to and from the PS, you get enormous bandwidth
from the parallel nature of the DDR through the system cache. However, for DMA,
the system bypasses the PS cache and goes directly from the DDR memory controller
to the AXI interconnect. The system is super fast!
The latest versions of the Ubuntu that runs on the A53 ARM chip (the 64 bit chip) boots up
with a default of 128MB of contiguous memory, all in the DDR. 128MB spread over 4 chips
is 32MB per chip, and each chip contains 1G (1024MB actually) of memory. So a contiguous
buffer of 128MB is only utilizing 32M/1024M = 3.1&percent; of the capacity of each
DDR chip. The rest can be used for general-purpose Linux memory like paging, processes,
kernel structures, etc.
If we were to increase the contiguous memory allocation to 1GB, a factor of 8, then
we would be reserving roughly 25&percent; of the memory for a contiguous buffer,
and if each buffer was created with 32 bits, we could have up to 512M of contiguous
memory for that buffer.
To confirm how much contiguous memory has been allocated, boot up the RFSoC, and at
the Linux prompt, type
All you need to do to increase contiguous memory is to create a file in /boot that
gets read at boot time, and put the modifications there. So it's quite easy:
If you want to see the typical stdout stream to a terminal while the thing boots up (and this
can be useful for when the thing shuts down), you can do the following:
Back to top
The RealDigital 4x2 board has a ZYNQ Ultrascale+ RFSoC ZU48DR chip made by AMD/Xilinx, and that
chip has electronics for 8 DACs and 8 ADCs. However, the RealDigital RFSoC board only allows
the use of 2 DACs and 4 ADCs. Each of the 2 DACS on that board
can be run with sampling times of up to 9.85 GSps,
and 4 ADCs with sampling times of 5 GSps.
Having ADCs that can digitize so fast allows radio processing to all be done in software.
The incoming signal, consisting of a carrier frequency in the GHz range with lower frequency
modulation, can be digitized directly, and Fourier analyzed to extract the modulation, with no
need for the usual heterodyne or superheterodyne downmixing.
The RF Converters on the ZYNQ chip are fully
integrated into the FPGA as described in the following figure:
NCO stands for numerically controlled oscillator, which means the carrier wave, and the converter
will generate both the in-phase (I, a cosine) with the quadrature (Q, sine) carrier wave. For the
DAC, you input a waveform, with separate I and Q parts, and the converter will multiply the carrier
I with the waveform I, same with Q, and mix the two together.
For the ADC, it will mix the carrier (from the NCO) I and Q with the incoming waveform to recover
the modulation I and Q parts.
The NCO is worth understanding: how does it produce a sine and cosine with arbitrary frequencies
without tuning an analog oscillator? The way it works is complicated, but you can get a good idea from
the following: imagine a lookup table (LUT) that has $2^{40}$
addresses, and at each address is a value such that if you plot that value for all
$2^{40}$ addresses, you'd see one period of a pretty high resolution cosine wave. Then you have a
40-bit register (called a "phase accumulator")
that points to one of the values in the table. The value that the phase accumulator points to
is the value that goes to the DAC for output conversion.
If you had a clock that would run at 9.85 GHz, and every tick of that clock you increment
the phase accumulator by 1/2 * $2^{40}-1$,
then the analog output from the DAC would change at 9.85 GHz, from $+$ to $-$ and generate
a wave at 1/2*9.85 GHz = 4.925 GHz. If on the other hand you increment the phase
accumulator by 1, then it would take $2^{40}$ ticks to go through an entire cycle,
which gives you an output frequency of $9.85 GHz/2^{40} = 8.96 mHz$ (milliHerz),
and that would look pretty continuous since the phase would only be changing by 1 part
in $2^{40}$ on each clock cycle.
Now let's say that you wanted the output of the DAC to change slower such that the
outgoing analog carrier wave was at, for example, 100MHz. You can do this easily by
changing the value you increment the phase accumulator. So to get a 100MHz tone,
you increment the phase accumulator by a value of $(100\times 10^6)/(9.85 \times 10^9)
\times 2^{40}$ which comes to $1.116\times 10^{10}$, which in hex is 0x$2995710E5$. Voila,
you can generate any frequency you want to within 8.96 mHz resolution.
This is basically how it works, except that in actuality it employs a great deal of tricks.
For instance, the cosine wave is very symmetric over the 4 quarters of a cycle, and there
are ways to reduce the number of values in the LUT by a great deal (imagine the graduate
students with theses that worked on this problem!).
Back to top
On the board there is a 30pin 2-row connector at the bottom right with the labels
"PMODB" on the left and "PMODA" on the right.
The following table gives the mapping for what pins on the connector are connected to
what FPGA IO pin number:
The 30-pin connector is constructed so that it looks like 2 "standard" 12-pin
PMOD connectors (that's what PmodA and PmodB label) separated by 2 rows of 3 pins
in between. That standard, which is probably
not universal but seems to be generally accepted,
has 2 rows of signals, with 4 signals in the first right-most pin followed by ground
and power (usually 3.3V). The pins are usually labeled with the top right ("Pin 1") as pin 1,
followed by 2, 3, 4, followed by ground on pin 5, and power on pin 6.
The bottom row starts with pin 7, with pin 11 as ground and 12 as power.
It's probably not so important to make this 30-pin connector look like 2 PMOD
connectors with 6 extra signals, since what we will probably be using this connector for
is to bring out signals for debugging. So let's make a labeling scheme that matches
a 22-bit vector (call it PMOD[21:0]) that gets translated onto this connector such that
PMOD[0] is the upper right, and PMOD[11] is the lower right, with numbers increasing to the
left and skipping the 4 grounds and 4 power pins.
So the connector looks like this:
Then we can define a 22-bit vector and map the FPGA IO pins to that vector in
the constraints file.
The constraints file that you will have to edit should have the following (the last 2 or 3
constraints are just generally good things to enable). Note that the IOSTANDARD property
sets the logic levels, which here are 1.8V separating 0 and 1, although the
connector can supply 3.3V logic.
Each of these data pins are connected in a pretty funky labeling on the schematic,
because they want to
be able to accommodate 2 12-pin PMOD connectors, and in such connectors, the pin
assignments run 1-6 on the top, right to left, then 7-12 on the bottom, left to
right. But let's make it simpler and remap them by defining the PMOD 30-pin connector
that has pin 1 in the upper right, with odd number pins on the top and even numbers
on the bottom. That would give us grounds on pins 9 and 10 on the right and 27, 28
on the left, and 3.3V on pins 11 and 12 on the right and 29 and 30 on the left.
To send a signal to one of the data pins, you first have to create the output port
on the project diagram. First, right click somewhere on the diagram,
and select "Create Port". In the window that pops up, give it a name, change the
"Direction" to Output, and change the "Type" to Data or Clock, the 2 most common
outputs. Then hit OK. That will instantiate an output port (this one is named
"test") that will look like this:
Next, we have to instantiate an output buffer, something that will take a signal
from somewhere and drive the output. Right click on an empty part of the diagram,
and select "Add IP" (or click the boldface $+$ sign on the menu bar under
"Diagram") and search for "Utility Buffer", and double click. That will bring up
a new circuit that will look like this:
To configure the buffer, double click and you should see a configuration window
that looks like this:
There are all kinds of buffers, and you can look them up but probably all you
will need for simple outputs are either "BUFG". I've found this to be adequate
for signals, even clocks as long as they aren't super fast (GHz).
The last thing you do on the diagram schematic entry
is to use the mouse to click on the signal you want to
output, and drag the connection to the buffer input, and likewise for the buffer
output to your output port.
Back to top
There's an IP called "Processor System Reset" that is quite useful, and you will see a lot
of these in the project. This IP is used to generate reset signals for the various
components in a project, and ensures that that resets are all synchronized correctly
during startup and when resets occur. It is also very useful for simulation, as most
simulation tools won't tell you how a wire or reg changes state without knowing its initial
value, which is determined via the reset.
The RealDigital 4x2 is a complex board with lots of parts that Vivado needs to know about.
All of these can be loaded into Vivado so that you can select the entire board in the
"Default Parts" panel when you build the project. To do this you do the following:
Back to top
There are 2 basic ways to save the project in Vivado: you can archive the project into a zip
file, or you can create a tcl script that can be used to regenerate the project (explained below).
Archiving is good for preserving the important project files into a zip file, and storing it. The
files that get archived are the output of the Vivado build process (synthesis, implementation, and
bitstream generation). You make the zip archive by going through the File/Project/Archive menu.
There used to be a way in the program ISE, before Vivado, to "clean" unnecessary files but Vivado
doesn't seem to have such a command. There might be a way to do it but it's probably overly
complicated. Anyway by archiving you can copy the project to another computer, unzip, and then
use Vivado on that computer.
There are 2 issues with archving: the zip file can be large; and it's difficult to use the zip file
on any version of Vivado other than the version that made the project that was zipped. The alternative
is to use tcl, the scripting language that Vivado uses.
Go into the tcl command line (click on "Tcl Console" tab at the bottom panel of Vivado), and
issue the following command:
will produce a file with filename name.tcl that contains a script that can be used to rebuild
the project from scratch by any version of Vivado (within reason, has to support all the IPs etc).
So for example, say you build the project with Vivado 2024.2 and want to rebuild it using Vivado 2022.1
on the same computer. You make the name.tcl using 2024.2, then run 2022.1, hit
"Create Project", set the project name and location in the "New Project" panel and hit Next. Skip
the "Project Type" panel (just hit Next) and "Add Sources", and "Add Constraints (optional)" panels
until you get to the "Default Part" panel. Here you click on "Boards", and search for 4x2 in the
Search window (about halfway down). Click on "Zynq UltraScale+ RFSoC 4x2 Development Board" when
that pops up (see 4x2parts if you don't see the 4x2). Note that clicking
on that has the annoying habit of popping up a browser window, which you can ignore and go back
to Vivado. Hit Next and that brings you into the Summary window. Hit Finish. That runs Vivado
where you will see all the familiar panels. Click on "Tcl Console" towards the bottom, and that
brings up the console and a text window. Type:
(or change "./" to where you put name.tcl) and it will build the project for you. Voila.
However, there is a caveat: any source file must be copied with the .tcl file and input by hand.
That means any verilog or vhdl source, and any constraint file that sets ports etc (*.xdc files).
Back to top
The 4x2 ARM chip that is running all the PYNQ software is the 64 bit chip, running Ubuntu Linux.
If you move the power switch on the board from ON to OFF to power it down, you could catch it in
a state of doing something that will result in the SD card becoming read-only on the next powerup.
So best to open up a terminal window in the Jupyter notebook (click on the "New Launcher") button,
which is a big blue rectangle with a "+" sign) and that brings up a new Launcher. Click on
the "Terminal" icon, that will log you into the ARM chip. Type
and wait a few seconds, then you can safely power off the board.
Back to top
There are a handful of good tutorials out there, and I will try to collect them here.
Table of Contents
Introduction
grep
and it should print out a string with
<SYSTEMINFO ARCH="zynquplusRFSOC" BOARD="realdigital.org:rfsoc4x2:part0:1.0" DEVICE="xczu48dr" NAME="GPIO" PACKAGE="ffvg1517" SPEEDGRADE="-2"/>
So you can see this board "thinks" its speed is -2, and it likely is.
RealDigital 4x2
-1ZCU208
-2
64-bit ARM 1.2GHz 1.333GHz
32-bit R5 533MHz 600MHz
UltraRAM clock 575MHz 600MHz
DDR4 I/O Up to 2400Mbps Up to 2666Mbps
GTY serial
transceiver line speed16-28Gbps 32.75Gbps

You can get more information of exactly what build you are running via
cat /etc/os-release
NAME="PynqLinux"
VERSION="3.0 (Carlisle)"
ID=pynqlinux
ID_LIKE=ubuntu
PRETTY_NAME="PynqLinux, based on Ubuntu 22.04"
VERSION_ID="3.0"
HOME_URL="https://www.pynq.io/"
SUPPORT_URL="https://discuss.pynq.io/"
BUG_REPORT_URL="https://www.pynq.io"
PRIVACY_POLICY_URL="https://www.pynq.io"
VERSION_CODENAME=Carlisle
UBUNTU_CODENAME=jammy
In the above, VERSION_CODENAME=Carlisle means that we are using PynqLinux 3.1.1, but
"VERSION=3.0 (Carlisle)" is wrong, because whoever built 3.1.1 didn't update the
"VERSION" number for some reason. You can check this by opening up a Jupyter
notebook after bootup and typing this:
import pynq
print(pynq.__version__)
It should return 3.1.1, which is Carlisle.

Backup from Zynq
/home/xilinx/jupyter_notebooks/ADMX
scp -r xilinx@192.168.3.1:/home/xilinx/jupyter_notebooks/ADMX .
It will prompt you for the password, which is "xilinx" and after you enter that, the
entire directory will be backed up.
Processors
The 64-bit A53 chip is the application processor, running a full Ubuntu Linux kernel and supporting
the PYNQ environment. It handles high-level tasks, for instance the Jupyter notebooks, file I/O,
networking, etc. It is optimized for throughput and general-purpose computing and is the one
you interact with when you SSH in, or use the Jupyter notebook to run Python scripts
or to open up a terminal window.
So you use the A53 for the Python data analysis, FPGA loading and configuration, DMA from memory
into the FPGA and RF Converter, etc.
Clocking

and the clock outputs include:
RF Converter clock
Why such arbitrary frequencies like 122.88MHz and 7.68MHz? It's because in the world of telecom,
especially LTE and 5G, 491.52MHz is one of the standard clock frequencies. The reason for such
odd numbers: LTE starts with a bunch of subcarriers spaced by Δf = 15kHz, and if there are
2048 subcarriers, then the sampling rate has to be 2048×15kHz=30.72MHz. Then 7.68MHz is
30.72/4, and 122.88MHz is 30.72*4, and so on. Welcome to the world of telecom, which is really
what the RFSoC boards are built for (not to find axions!!!)
from pynq import Overlay, MMIO, allocate
from xrfdc import RFdc
from pynq.overlays.base import BaseOverlay
import time
import xrfclk
import numpy as np
from pynq import Clocks
class Gen(BaseOverlay):
def __init__(self,NCO=False,ext_clock=True):
Overlay.__init__(self,'cyclic_clock.bit')
if self.is_loaded():
self.i2c_initialized = False
self.display_port_initialized = False
self.radio = self.usp_rf_data_converter_0
self.xrfdc_alias = MMIO(self.ip_dict['usp_rf_data_converter_0']['phys_addr'],
self.ip_dict['usp_rf_data_converter_0']['addr_range']).array
self.init_rf_clks()
self.freq_a = 0.01
self.radio.dac_tiles[2].blocks[0].MixerSettings['EventSource'] = 0
self.freq_b = 0.01
self.radio.dac_tiles[0].blocks[0].MixerSettings['EventSource'] = 0
def set_external_rf_clks(self):
for lmk in xrfclk.lmk_devices:
with open(lmk['spi_device'], 'rb+', buffering=0) as f:
data = b'\x01\x47\x0A' # Only works for LMK0482x series
f.write(data)
def set_internal_rf_clks(self):
for lmk in xrfclk.lmk_devices:
with open(lmk['spi_device'], 'rb+', buffering=0) as f:
data = b'\x01\x47\x1A' # Only works for LMK0482x series
f.write(data)
@property
def freq_a(self): # MHz
return self.radio.dac_tiles[2].blocks[0].MixerSettings['Freq']
@freq_a.setter
def freq_a(self,freq): # MHz
self.radio.dac_tiles[2].blocks[0].MixerSettings['Freq'] = freq
self.radio.dac_tiles[2].blocks[0].UpdateEvent(1)
@property
def freq_b(self): # MHz
return self.radio.dac_tiles[0].blocks[0].MixerSettings['Freq']
@freq_b.setter
def freq_b(self,freq): # MHz
self.radio.dac_tiles[0].blocks[0].MixerSettings['Freq'] = freq
self.radio.dac_tiles[0].blocks[0].UpdateEvent(1)
print("Clock before overclocking: "+str(Clocks.fclk0_mhz)+" MHz")
gen = Gen()
gen.set_external_rf_clks()
Clocks.fclk0_mhz = 250
print("Clock after overclocking: "+str(Clocks.fclk0_mhz)+" MHz")
print("all done")
Install Linux and boot up
RFSoC-PYNQ
Version 3.0.1 or 3.1.1
Wait another 20 seconds and then the board will try to connect to the computer over
USB3 and establish itself as a web address. You should see something like this on the LCD
display:
IP Addr(usb0):
192.168.3.1
Go to your computer, fire up a browser, and in the url type
192.168.3.1/lab
Log in as user xilinx, password xilinx, (no comma) and you should be good to go.
Increasing contiguous memory on the A53 processor
cat /proc/meminfo | grep -i cma
It will return something like this:
CmaTotal: 131072 kB
CmaFree: 120680 kB
The "CmaTotal" of 131072 can be converted from kB to MB by dividing by 1024 to get 128 MB.
The Linux version of PynqLinux here is based on Ubuntu 22.04 Jammy, and it boots up via a
U-boot script (there are alternatives, but this is what PynqLinux uses).
You can know this by looking at the directory /boot/ via
ls -la /boot/
drwxr-xr-x 3 root root 16384 Jan 1 1970 .
drwxr-xr-x 19 root root 4096 Nov 21 17:57 ../
-rwxr-xr-x 1 root root 1683736 Oct 23 2022 BOOT.BIN*
-rwxr-xr-x 1 root root 1726 Oct 23 2022 boot.py*
-rwxr-xr-x 1 root root 2779 Oct 23 2022 boot.scr*
-rwxr-xr-x 1 root root 23123516 May 25 2024 image.ub*
-rwxr-xr-x 1 root root 89 Oct 23 2022 REVISION*
drwxr-xr-x 2 root root 4096 May 26 2024 "System Volume Information"
That you see BOOT.BIN and boot.scr is telling you that the system uses the U-Boot facility.
cd /boot
sudo nano uEnv.txt
and add this line (one line, be careful!)
bootargs=root=/dev/mmcblk0p2 rw earlyprintk rootfstype=ext4 rootwait devtmpfs.mount=1 uio_pdrv_genirq.of_id="generic-uio" clk_ignore_unused cma=512M
Then save and exit (control x) and reboot. This increases the contiguous memory to 512M,
but you can go higher (e.g. change "512M" to "2G"). When I did that on the 4x2 board,
I now see the following:
CmaTotal: 524288 kB
CmaFree: 518000 kB
And 524288 divided by 1024 is 512, which is what I set it to.
Getting STDOUT Linux bootup output
/dev/cu.usbserial-8873392300A50
/dev/cu.usbserial-8873392300A51
/dev/tty.usbserial-8873392300A50
/dev/tty.usbserial-8873392300A51
The difference between tty and cu is that tty listens without responding but
cu will respond, so we want tty. Note the 2 tty numbers. I don't know how
to tell which one is the right one, but the numbers will differ by 1 usually.
You will need these numbers below (they won't be the same as above).
RF Converter

Driving PMOD output ports

TOP 3.3V GND AV13 AU13 AR13 AW13 AW14 AW15
AW16 3.3V GND AK17 AJ16 AG17 AF16
BOT 3.3V GND AU14 AT15 AP14 AU15 AT16 AV16 AR16
3.3V GND AK16 AH17 AF17 AF15
Using the 30-pin connector
TOP 3.3V GND 10=AV13 9=AU13 8=AR13 7=AW13 6=AW14 5=AW15
4=AW16 3.3V GND 3=AK17 2=AJ16 1=AG17 0=AF16
BOT 3.3V GND 21=AU14 20=AT15 19=AP14 18=AU15 17=AT16
16=AV16 15=AR16 3.3V GND 14=AK16 13=AH17 12=AF17 11=AF15
set_property PACKAGE_PIN AF16 [ get_ports "PMOD[0]" ]
set_property PACKAGE_PIN AG17 [ get_ports "PMOD[1]" ]
set_property PACKAGE_PIN AJ16 [ get_ports "PMOD[2]" ]
set_property PACKAGE_PIN AK17 [ get_ports "PMOD[3]" ]
set_property PACKAGE_PIN AW16 [ get_ports "PMOD[4]" ]
set_property PACKAGE_PIN AW15 [ get_ports "PMOD[5]" ]
set_property PACKAGE_PIN AW14 [ get_ports "PMOD[6]" ]
set_property PACKAGE_PIN AW13 [ get_ports "PMOD[7]" ]
set_property PACKAGE_PIN AR13 [ get_ports "PMOD[8]" ]
set_property PACKAGE_PIN AU13 [ get_ports "PMOD[9]" ]
set_property PACKAGE_PIN AV13 [ get_ports "PMOD[10]" ]
set_property PACKAGE_PIN AF15 [ get_ports "PMOD[11]" ]
set_property PACKAGE_PIN AF17 [ get_ports "PMOD[12]" ]
set_property PACKAGE_PIN AH17 [ get_ports "PMOD[13]" ]
set_property PACKAGE_PIN AK16 [ get_ports "PMOD[14]" ]
set_property PACKAGE_PIN AR16 [ get_ports "PMOD[15]" ]
set_property PACKAGE_PIN AV16 [ get_ports "PMOD[16]" ]
set_property PACKAGE_PIN AT16 [ get_ports "PMOD[17]" ]
set_property PACKAGE_PIN AU15 [ get_ports "PMOD[18]" ]
set_property PACKAGE_PIN AP14 [ get_ports "PMOD[19]" ]
set_property PACKAGE_PIN AT15 [ get_ports "PMOD[20]" ]
set_property PACKAGE_PIN AU14 [ get_ports "PMOD[21]" ]
set_property IOSTANDARD LVCMOS18 [ get_ports "PMOD[*]" ]
# configure unused IO pins to have internal pull-up resistors
set_property BITSTREAM.CONFIG.UNUSEDPIN PULLUP [current_design]
# enable the over-temperature shutdown features
set_property BITSTREAM.CONFIG.OVERTEMPSHUTDOWN ENABLE [current_design]
# compress the bitstream to make it smaller
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
The pins are organized so that you can put a standard 12-pin PMOD connector into
the right-most pins (PmodA, 1st 6 pairs of top/bottom pins) and the left-most pins
(PmodB, last 6 pairs of top/bottom pins).
That leaves 3 pairs of top/bottom pins in the middle. Each of these pins are routed to
an IO pin on the FPGA, specified on page 16 of the
Reference Manual.



Implementing Resets
Importing 4x2 board parts into Vivado
set_param board.repoPaths RFSoC4x2-BSP-2020.2/board_files/rfsoc4x2/1.0/
xhub::refresh_catalog [xhub::get_xstores xilinx_board_store]
Saving the project
write_bd_tcl -make_local -no_ip_version name.tcl
source ./name.tcl
Powering off the 4x2 board
shutdown now
Tutorials
Drew Baden
Last update June 4, 2026
All rights reserved. No part of this publication may be reproduced, distributed, or transmitted in any
form or by any means, including photocopying, recording, or other electronic or mechanical methods, without
the prior written permission of the publisher, except in the case of brief quotations embodied in critical
reviews and certain other noncommercial uses permitted by copyright law.