RealDigital RFSoC 4x2 Tutorial

Table of Contents


Introduction

This web page is meant to be an introduction to how to use the RealDigital 4x2 RFSoC development board. 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 might be something on PYNQ here. A good place to start for a PYNQ tutorial might be here

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 mdern.

You can find the reference manaual (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, better internal clocking, etc. The data converters allow you to modulate a carrier wave with a lot of flexibility.

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:

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, derived from the old Wishbone 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.

For the DAC, what the chip does internally is to produce digital values representing an I and Q periodic wave at whatever carrier frequency you want, and multiply the I with the incoming modulation I and Q digital values that come in over the AXI bus. The modulated I and modulated Q are then added together and sent to the DAC for digital-to-analog conversion. (End of Page) 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:

Backup from Zynq

All Jupyter notebooks are kepts 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:

Tutorials

There are a handful of good tutorials out there, and I will try to collect them here.

Install Linux and boot up

In the top image you will see an SD card input at the top left of the 1st 3 push buttons. The RealDigital 4x2 board ships with an SD card that is supposed to be loaded, but it doesn't work for some odd reason. So you will have to build your own from scratch. Here's a tutorial on how to do it:

If all goes well, 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.

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:

When the board boots, wait around 30 seconds, then you should see on the LCD display the following:

RFSoC-PYNQ
Version 3.0.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.

RF Converter

The RealDigital 4x2 board has data converters integrated with the FPGA and the ARM processors. The converters do everything needed to produce 2 DACs that with sampling times of 9.85 GSps, and 4 ADCs with sampling times of 5 GSps. These devices are used in the context of software radio, which means the specialize at mixing signals with carrier waves, as 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, sin) 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: imaging 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 a pretty high resolultion cosine wave. Then you have a 40-bit register (called a "phase accumulator") that points to one of the values in the table, and that value is taken out and shoved into the phase accumulator DAC. 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*98.5 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 $4.925 GHz/2^{40} = 4.48 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)/(4.925 \times 10^9) \times 2^{40}$ which comes to $2.233\times 10^{10}$, which in hex is $532AE24F0$. Voila, you can generate any frequency you want to within 4.48 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!).

Driving PMOD output ports

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:

TOP3.3VGNDAV13AU13AR13AW13AW14AW15 AW16 3.3VGNDAK17AJ16AG17AF16
BOT3.3VGNDAU14AT15AP14AU15AT16AV16AR16 3.3VGNDAK16AH17AF17AF15

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 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.

Using the 30-pin connector

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:

TOP3.3VGND1o=AV139=AU138=AR137=AW136=AW145=AW15 4=AW163.3VGND3=AK172=AJ161=AG170=AF16
BOT3.3VGND21=AU1420=AT1519=AP1418=AU1517=AT16 16=AV1615=AR163.3VGND14=AK1613=AH1712=AF1711=AF15

The contstraints file that you will have to edit should have the following (the last 3 constraints are just generally good things to enable):

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 feathers
set_property BITSTREAM.CONFIG.OVERTEMPSHUTDOWN ENABLE [current_design]
# compress the bitstream to make it smaller
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
Then we can define a 22-bit vector and map the FPGA IO pins to that vector in the constraints file, which would look like this: 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.

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.

Each of the 22 pinouts on the PMOD connector are connected to an IO pin on the FPGA, The relevant table is reproduced below:

So now you have an output port, and in our example, the name is "test". This port exists in the PL, and you have to connect to the right IO pin on the FPGA. , and each of these 22 pinouts are connected to Those connections

Implementing Resets

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.

AXI Fifo

When we want to send a waveform from the ARM chip to the RF converter, we use DMA (explained elsewhere). The DMA engine sends data into the FPGA, which sends it into a FIFO at a high speed. The output of the FIFO will be a 32 bit word, with the lower 16 bits being the input to the I port on the RF converter and the upper 16 bits being the input to the Q port.

To make a new FIFO, right click on any empty space on the design, and select "Add IP...". In the "Search" window type "FIFO generator" and select "FIFO Generator" and hit return. That brings up a new "FIFO Generator" block. Double click. It should look like this:

Here's how you configure it in each of the tabs:

Then hit OK on the bottom right, and that gives you a FIFO that should look like this:

Now let's discuss the inputs and outputs. But first, remember that we want this FIFO to be written into by the DMA engine, and read out by the RF Converter. So we want the FIFO to have separate read and write clocks.

To configure the FIFO, launch the IP tool, and find "FIFO Generator". There will be 4 tabs: Basic, AXI4 Stream Ports, Config, and Status Flags with a 5th tab called Summary.

Importing 4x2 board parts into Vivado

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:

  1. First, download the Board Support Package (BSP) files from here
  2. Unzip, which will make a directory called "RFSoC4x2-BSP-2020.2" somewhere that has the BSP files. Note the location of that directory, and run the following 2 commands in the Vivado Tcl console:
    		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

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:

	write_bd_tcl -make_local -no_ip_version name.tcl

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 usign 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 Vivao 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:

	source ./name.tcl

(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).

Powering off the 4x2 board

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

	shutdown now

and wait a few seconds, then you can safely power off the board.

Back to top


Drew Baden  Last update May 7, 2024 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.