This tutorial shows how to do an HW design and code a SW application to make use of AMD Xilinx Zynq-7000 XADC. We will also see how to use the DMA to transfer data from the XADC into Zynq CPU's memory and stream data to a remote PC over the network.
This is the third part of the tutorial (the last one). In it, I describe an XADC demo application I created.The first part covered the XADC's concepts, and the second part covered the HW design in Vivado.
In this tutorial, I'm using the Digilent board Cora Z7-07S. However, all the principles described here can be used on any other Zynq-7000 board.
I'm using Vitis Classic 2024.1.1. Nevertheless, the same steps also work in Vitis 2023.1 and Vitis Classic 2023.2.
I also provide instructions on how to build the demo application in Vitis 2024.1.1 (a.k.a. Vitis Unified 2024).
I'm using Vitis Classic 2024.1 in this chapter. If you want to use Vitis 2024.1.1 (a.k.a. Vitis Unified 2024), go to this readme file and then continue with the chapter How to use the application.
Start Vitis Classic 2024.1.
Create a new platform project using the HW export XSA file we generated in the second part of the tutorial.
Make sure to select freertos_10_xilinx as the operating system.
We will be using the lwIP library to stream XADC data over the network. Therefore, we must enable the lwIP in the Board Support Package settings (BSP).
The following settings are then needed in the lwIP BSP configuration:
- Set api_mode to "SOCKET API" because this api_mode is required for a FreeRTOS or stand-alone application.
- Set dhcp_options/lwip_dhcp to true because my demo application is using DHCP to obtain an IP address.
Create a new application project.Make sure to select the "Empty Application (C++)" in the last step of the application project creating wizard.
Copy all source files from the XADC_tutorial_app folder of my GitHub repository into the src folder of the application project in Vitis.
Let me briefly explain what source files we have:
FileViaSocket.h, FileViaSocket.cpp
- Definition of the C++ ostream class, which the demo application uses to send data over the network. I copied the files from another repository of mine.
button_debounce.h, button_debounce.cpp
- A C++ class that the demo application uses for debouncing buttons (i.e., to ensure that the app gets a filtered signal from the buttons for smooth control).
- Copyright © 2014 Trent Cleghorn. I copied the files from his repository.
main.cpp – The main source file of the demo application.
- The definition of a FreeRTOS thread, which initiates the network and handles network operation.
I derived it from a sample project provided by AMD Xilinx. - Copyright © 2024 Viktor Nikolov
Copyright © 2018-2022 Xilinx, Inc.
Copyright © 2022-2023 Advanced Micro Devices, Inc.
The project should be built without errors. You may see two or three warnings coming from the platform source files (not files in the app's src folder). These can be ignored.
How to use the applicationThe HW design and the demo application from this tutorial allow 1 Msps digitalization on the differential dedicated analog input channel VP/VN (labeled V_P/V_N on the Cora Z7 board) and the unipolar auxiliary channel VAUX[1] (labeled A0 on the board).
To see interesting results, you need to use a signal generator. Connect a suitable differential signal to pins V_P/V_N (e.g., a signal I used in this chapter) and a unipolar signal to pin A0.
Caution:
The voltage on the V_P and V_N pins must always be within a range from 0 V to 1.0 V with respect to the board's GND. Also, the differential VP − VN must be within the range of ±0.5V.
The voltage on the pin A0 must always be positive and not greater than 3.3 V.
The application sends data samples over the network to a server, which is the Python script file_via_socket.py.
You must specify the IP address of the server in the constant in main.cpp. It is this line at the beginning of the main.cpp:
/* Specify your actual server IP address */
const std::string SERVER_ADDR( "192.168.44.10" );
The server writes the XADC samples (a list of voltage values) to a text file. Each set of samples is written to a new file.
The standard name of the file the server creates looks like this: via_socket_240324_203824.6369.txt
Part of the name in italics is the date and time stamp.
Depending on your Python installation, run the script with the commandpython3 file_via_socket.py [params]
orpython file_via_socket.py [params]
.
In typical use, you will want to specify the output folder for the files. For example:
>python file_via_socket.py --path c:\Temp\XADC_data
Waiting for connection on 0.0.0.0:65432
(Press Ctrl+C to terminate)
The default port the script listens on for connections is 65432, and the default bind IP address is 0.0.0.0 (i.e., the script listens on all the configured network interfaces and their IP addresses).
Typically, you have just one IP address assigned to the Ethernet port of your PC. Use the command ipconfig
(on Windows) or ip a
(on Linux) to get this address and enter it as the SERVER_ADDR
constant at the beginning of the main.cpp.
Make sure that the firewall on your PC allows incoming connections to Python on the given IP address and port.
I tested the script on Windows 11 and Ubuntu 22.04.
To get the full list of available parameters, run python file_via_socket.py --help
.
Let me summarize. To successfully use the demo application, you need to perform these steps:
- Connect a suitable signal from a signal generator to Cora Z7 pins V_P and V_N or to pin A0 (or to both).
- Connect the network cable to the Cora Z7 board.
- Start the
python file_via_socket.py
on your PC as the server to receive digitized data samples. - Start a serial terminal application (e.g., PuTTY) and connect it to the USB serial port of the Cora Z7 board in your OS.
- Specify the IP address of your server in the constant
SERVER_ADDR
at the beginning of the main.cpp.
If you are unsure what your PC's IP address is, use the commandipconfig
(on Windows) orip a
(on Linux). - Build and run the application in Vitis.
After the application starts, you shall see the output in the serial terminal similar to this:
*************** PROGRAM STARTED ***************
------lwIP Socket Mode TCP Startup------
Start PHY autonegotiation
Waiting for PHY to complete autonegotiation.
autonegotiation complete
link speed for phy address 1: 1000
DHCP request success
Board IP: 192.168.44.39
Netmask : 255.255.255.0
Gateway : 192.168.44.1
***** XADC THREAD STARTED *****
will connect to the network address 192.168.44.10:65432
samples per DMA transfer: 1000
no averaging is used
calib coefficient ADC offset: FF9A (-7 bits)
calib coefficient gain error: 007F (6.3%)
press BTN0 to start ADC conversion
press BTN1 to switch between VAUX[1] and VP/VN inputs
VAUX[1] is activated as the input
Information under the header --lwIP Socket Mode TCP Startup--
comes from the lwIP network initialization and DHCP IP address assignment.
After the network initializes the thread controlling the XADC takes over and displays some basic information.
We see that 1000 XADC samples will be provided in each measurement (i.e., in each DMA transfer from the XADC). You can control this number of samples by modifying the macro SAMPLE_COUNT
at the beginning of the main.cpp.
/* Number of samples transferred in one DMA transfer. Max. value is 33,554,431 */
#define SAMPLE_COUNT 1000
- Note: Yes, I successfully tested the digitization of 33, 554, 431 samples. 😃 It takes 33.5 seconds to record the XADC data and then about 1 minute 45 seconds to convert raw values to voltage and transfer the data to the PC (tested on Cora Z7, compiled with the highest optimization level -O3).
We also see in the console output that XADC will not use any averaging. This is controlled by defining the value of the macro AVERAGING_MODE
at the beginning of the main.cpp. You can select one of the four options:
/* Set XADC averaging.
* Leave one of the lines below uncommented to set averaging mode of the XADC. */
#define AVERAGING_MODE XSM_AVG_0_SAMPLES // No averaging
//#define AVERAGING_MODE XSM_AVG_16_SAMPLES // Averaging over 16 acq. samples
//#define AVERAGING_MODE XSM_AVG_64_SAMPLES // Averaging over 64 acq. samples
//#define AVERAGING_MODE XSM_AVG_256_SAMPLES // Averaging over 256 acq. samples
The next information in the console output tells us that the value of XADC's Offset Calibration Coefficient ix 0xFF9A, which translates to -7 bits of correction. You may observe that this value changes slightly with each application run.
The value of XADC's Gain Calibration Coefficient is shown as 0x007F, which translates to a 6.3% correction (the maximum possible value). This is expected on the Cora Z7 board for reasons I explained in detail in the chapter XADC autocalibration.
Lastly, the console output tells us what the buttons do and that the auxiliary channel VAUX[1] is activated as input for the XADC measurement.
When we press the board's button labeled BTN0, the application will store 1000 XADC samples in memory (using DMA), display values of the first 8 samples on the terminal, and send all 1000 samples to the server, where a file of 1000 lines will be created.
***** XADC DATA[0..7] *****
1.878496
1.651487
1.448801
1.277734
1.156933
1.086398
1.074237
1.120449
sending data... sent
Controlling the XADC from the PSLet me explain the aspects of controlling the XADC from the PS in more detail. I will use code snippets from the main.cpp.
The initialization of the XADC is very similar to the other Xillinx subsystems:
#include "xsysmon.h"
XSysMon XADCInstance; /* The XADC instance */
XSysMon_Config *ConfigPtr; /* Pointer to the XADC configuration */
XStatus Status;
/* The macro XPAR_XADC_WIZ_0_DEVICE_ID comes from xparameters.h */
ConfigPtr = XSysMon_LookupConfig( XPAR_XADC_WIZ_0_DEVICE_ID );
if( ConfigPtr == NULL ) { /* raise an error*/ }
Status = XSysMon_CfgInitialize( &XADCInstance, ConfigPtr, ConfigPtr->BaseAddress );
if( Status != XST_SUCCESS ) { /* raise an error*/ }
After the initialization, I disable XADC features, which we do not need in this demo application:
/* Disable all interrupts */
XSysMon_IntrGlobalDisable( &XADCInstance );
/* Disable the Channel Sequencer (we will use the Single Channel mode) */
XSysMon_SetSequencerMode( &XADCInstance, XSM_SEQ_MODE_SINGCHAN );
/* Disable all alarms */
XSysMon_SetAlarmEnables( &XADCInstance, 0 );
/* Disable averaging for the calculation of the calibration coefficients */
/* Read Configuration Register 0 */
u32 RegValue = XSysMon_ReadReg( XADCInstance.Config.BaseAddress, XSM_CFR0_OFFSET );
/* To disable calibration coef. averaging, set bit XSM_CFR0_CAL_AVG_MASK to 1 */
RegValue |= XSM_CFR0_CAL_AVG_MASK;
/* Write Configuration Register 0 */
XSysMon_WriteReg( XADCInstance.Config.BaseAddress, XSM_CFR0_OFFSET, RegValue );
The XADC Averaging is set using the following call.
XSysMon_SetAvg( &XADCInstance, AVERAGING_MODE );
The macro AVERAGING_MODE
is set to one of the values XSM_AVG_0_SAMPLES
(no averaging), XSM_AVG_16_SAMPLES
, XSM_AVG_64_SAMPLES
or XSM_AVG_256_SAMPLES
at the beginning of the main.cpp.
Because the board Cora Z7 doesn't provide an external voltage reference to the XADC, we must make sure to enable only usage of the Offset Calibration Coefficient by the following call (see details explained in the chapter XADC Autocalibration).
XSysMon_SetCalibEnables(&XADCInstance, XSM_CFR1_CAL_ADC_OFFSET_MASK |
XSM_CFR1_CAL_PS_OFFSET_MASK);
Before activating an analog input, we must set the ADCCLK divider ratio (see details explained in the chapter Clocking).
/* Set the ADCCLK frequency equal to 1/4 of the XADC input clock */
XSysMon_SetAdcClkDivisor( &XADCInstance, 4 );
The following call activates the auxiliary input VAUX[1] in the single channel unipolar continuous sampling mode.
XSysMon_SetSingleChParams(
&XADCInstance,
XSM_CH_AUX_MIN+1, /* == channel index of VAUX[1] */
false, /* IncreaseAcqCycles==false -> default 4 ADCCLKs used for
the settling;
true -> 10 ADCCLKs used */
false, /* IsEventMode==false -> continuous sampling */
false ); /* IsDifferentialMode==false -> unipolar mode */
The second parameter of XSysMon_SetSingleChParams() is the channel index. You can use macros XSM_CH_*
, which are defined in the xsysmon.h. Macro XSM_CH_AUX_MIN
is the index of VAUX[0]. By adding 1 to it, we get the index of VAUX[1]. The macro XSM_CH_VPVN
gives the index of the dedicated analog input channel VP/VN.
Next is the boolean parameter IncreaseAcqCycles
.Value false means that the default duration of 4 ADCCLK clock cycles is used for the settling period, so the acquisition takes 26 ADCCLK cycles in total. We use a 104 MHz XADC input clock in the HW design. We set the divider ratio to 4 by calling XSysMon_SetAdcClkDivisor(). This results in 26 MHz ADCCLK and thus 1 Msps sampling rate.
If the parameter IncreaseAcqCycles
was true, 10 ADCCLK cycles would be used for the settling period, thus extending the acquisition to 32 ADCCLK cycles. That would result in an 812.5 ksps sampling rate.
The HW design in this tutorial and the demo app are set to run the XADC at the maximum possible sampling rate of 1 Msps.To achieve other (i.e., lower) sampling rates, you need to set a suitable XADC Wizard input clock frequency in the HW design and a suitable ADCCLK clock divider ratio so the quotient of frequency and the ratio is 26 times the desired sampling rate.
E.g., to have a sampling rate of 100 ksps, you can set the Clocking Wizard, which feeds the XADC Wizard input clock, to 101.4 MHz and set the divider ratio to 39 (by calling XSysMon_SetAdcClkDivisor()).
101400 / 39 = 2600
2600 / 26 = 100 ksps
The next boolean parameter IsEventMode
specifies event sampling mode (value true) or continuous sampling mode (value false).
The last boolean parameter, IsDifferentialMode
, specifies bipolar mode (value true) or unipolar mode (value false). See the explanation of the two modes in this chapter.
In our design, the XADC starts sending the desired data samples over the master AXI-Stream of the XADC Wizard after we call XSysMon_SetSingleChParams() in the PS.
Let me explain in this chapter how we control the AXI DMA to get data from PL into the RAM of the Zynq ARM core.
The initialization of the DMA is like that of other Xillinx subsystems:
#include "xaxidma.h"
XAxiDma AxiDmaInstance; /* The AXI DMA instance */
XAxiDma_Config *cfgptr; /* Pointer to the AXI DMA configuration */
XStatus Status;
/* The macro XPAR_AXI_DMA_0_DEVICE_ID comes from xparameters.h */
cfgptr = XAxiDma_LookupConfig( XPAR_AXI_DMA_0_DEVICE_ID );
if( cfgptr == NULL ) { /* raise an error*/ }
Status = XAxiDma_CfgInitialize( &AxiDmaInstance, cfgptr );
if( Status != XST_SUCCESS ) { /* raise an error*/ }
We don't use AXI DMA interrupts in this demo application, so we disable them:
XAxiDma_IntrDisable(&AxiDmaInstance, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA);
XAxiDma_IntrDisable(&AxiDmaInstance, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);
We need to have a space in memory for the AXI DMA to load data into. The easiest way is to declare the data buffer as a global variable. This way, we don't need to worry about the FreeRTOS thread's stack size.
u16 DataBuffer[ SAMPLE_COUNT + 8 ] __attribute__((aligned(4)));
Important considerations go into declaring an array for receiving data from AXI DMA. Let me explain:
- Data type: In our case, the AXI-Stream data width is 16 bits. This is because the XADC Wizzard exposes a 16-bit wide master AXI-Stream interface, and it goes as the 16-bit stream all the way into the AXI DMA. So, we use the data type
u16
. - Array length: The number of samples we transfer in one DMA transfer is given by the macro
SAMPLE_COUNT
. However, there is a catch, which is why I recommend that you declare theDataBuffer
slightly larger than needed.
The AXI DMA loads data directly into the RAM. There is a data cache between the RAM and the CPU in play. To achieve proper results so the CPU "sees" the correct data, we flush the data cache into RAM by calling Xil_DCacheFlushRange() before the DMA transfer. We then invalidate the data cache by calling Xil_DCacheInvalidateRange() after the DMA transfer finishes so theDataBuffer
memory region is served to the CPU from RAM when read for the first time.
I faced strange errors in my testing when I used a biggerDataBuffer
. The problem went away when I declared theDataBuffer
slightly larger than needed. I think this is a bug or a limitation of the Xil_DCacheInvalidateRange().
My theory is that the issue is connected to the fact that the data cache is organized into so-called lines, which are 32 bytes long on the ARM Cortex-A9 CPU used in Zynq-7000. Cache invalidation is done by the 32-byte cache lines, not by the exact length ofDataBuffer
. I guess the problem I observed was caused by Xil_DCacheInvalidateRange() missing a cache line. - Memory alignment: Even though the AXI DMA can be configured to allow data transfer to an address that is not aligned to a word boundary, it's a good practice to declare the
DataBuffer
as aligned to a 32-bit word. This is what the GCC attribute definition__attribute__((aligned(4)))
does.
We tell the AXI DMA to start loading data into RAM by this call:
/* Just in case, flush data in DataBuffer, held in the CPU cache, to the RAM */
Xil_DCacheFlushRange( (UINTPTR)DataBuffer, sizeof(DataBuffer) );
/* Tell AXI DMA to start the data transfer */
XAxiDma_SimpleTransfer( &AxiDmaInstance, (UINTPTR)DataBuffer,
SAMPLE_COUNT * sizeof(u16), XAXIDMA_DEVICE_TO_DMA );
This call initiates the AXI DMA, but no data will start flowing yet. As I explained in the chapter DMA, we have the module stream_tlaster.v in our HW design to serve as a "valve" on the AXI-Strem between the XADC Wizard and AXI DMA.
We need to tell the module how many data samples we want to go through and then start the data flow. We do that by means of GPIO signals, which we connected to the stream_tlaster module in the HW design.
/* Set sample count to EMIO GPIO pins 55-80, which are connected to the
stream_tlaster module.
(The least significant EMIO GPIO pin 54 is the start/stop signal.) */
XGpioPs_Write( &GpioInstance, 2 /*Bank 2*/, SAMPLE_COUNT << 1 );
/* Set start signal of the stream_tlaster module to start generation of the
AXI-Stream of data coming from the XADC. */
XGpioPs_WritePin( &GpioInstance, 54, 1 /*high*/ );
/* Reset the start signal (it needed to be high for just a single PL clock cycle) */
XGpioPs_WritePin( &GpioInstance, 54, 0 /*low*/ );
After that, we wait in a while loop for the AXI DMA to finish the data transfer.
After the AXI DMA transfer finishes, we must invalidate the memory region of the DataBuffer in the data cache.
/* Wait till the DMA transfer is done */
while( XAxiDma_Busy( &AxiDmaInstance, XAXIDMA_DEVICE_TO_DMA ) )
vTaskDelay( pdMS_TO_TICKS( 1 ) ); /* Wait 1 ms */
/* Invalidate the CPU cache for the memory region holding the DataBuffer. */
Xil_DCacheInvalidateRange( (UINTPTR)DataBuffer, sizeof(DataBuffer) );
Note:
- In my code, I don't need to check how much data the AXI DMA actually transferred. This is because we have the stream_tlaster.v module in the HW design, which gives us the ultimate control over the AXI-Stream data flow. We control in the HW when the stream starts and how much data comes in it.
- This may not be the case in other HW designs. Post DMA transfer, the information about how many bytes were actually transferred (i.e., how many bytes came before AXI-Stream signal TLAST was asserted) is available in the AXI DMA's S2MM_LENGTH register. You can read this value by the following call:
u32 BytesTransferred = XAxiDma_ReadReg( AxiDmaInstance.RegBase,
XAXIDMA_RX_OFFSET+XAXIDMA_BUFFLEN_OFFSET );
Converting raw XADC data samples to the voltageTo convert the raw XADC data sample to the voltage, we must consider whether the XADC uses averaging. If a voltage divider is present on the XADC channel input, we must, of course, also consider the scaling factor.
The main.cpp of the demo application contains conversion functions for unipolar channel VAUX[1] and bipolar channel VP/VN.
I'm using conditional compilation based on the value of the macro AVERAGING_MODE
. When the XADC is set to use averaging, all 16 bits of the raw sample are used. Without averaging, only 12 bits are valid; the 4 least significant bits must be ignored.
Code snippets shown in this chapter are the versions when XADC averaging is not used.
Converting a raw sample to voltage is pretty straightforward for a channel in the unipolar mode:
/* Conversion function of XADC raw sample to voltage for the
unipolar channel VAUX[1] */
float Xadc_RawToVoltageAUX1(u16 RawData)
{
/* We use VAUX[1] as unipolar; it has the scale from 0 V to 3.32 V.
There is voltage divider of R1 = 2.32 kOhm and R2 = 1 kOhm on the input. */
const float Scale = 3.32;
/* When XADC doesn't do averaging, only the 12 most significant bits of
RawData are valid */
return Scale * ( float(RawData >> 4) / float(0xFFF) );
}
Converting a raw sample from a channel in the bipolar mode requires a bit of binary arithmetic.
The measuring range is not symmetrical around zero. It goes from -500.00 mV to 499.75 mV in 244 μV steps. See Figure 2-3 in the UG480.
/* Conversion function of XADC raw sample to voltage for the
bipolar channel VP/VN */
float Xadc_RawToVoltageVPVN(u16 RawData)
{
/* When XADC doesn't do averaging, only the 12 most significant bits of
RawData are valid */
if( (RawData >> 4) == 0x800 )
/* This is the special case of the lowest negative value
The measuring range in bipolar mode is -500 mV to 499.75 mV.*/
return -0.5;
float sign;
if( RawData & 0x8000 ) { /*Is sign bit equal to 1? I.e. is RawData negative?*/
sign = -1.0;
/* Get absolute value from negative two's complement integer */
RawData = ~RawData + 1;
}
else
sign = 1.0;
/* We are not using averaging, only the 12 most significant bits of
RawData are valid. */
RawData = RawData >> 4;
/* One bit equals to the reading of 244 uV. I.e., 1/4096 == 244e-6 */
return sign * float(RawData) * ( 1.0/4096.0 );
}
Project exportsThe repository's folder project_files provides the file vitis_export_archive.ide_Classic_2024.1.1.zip.
It contains the SW project export from Vitis Classic 2024.1.1.
To use the export, create an empty folder on your PC and open it as a workspace in Vitis Classic 2024.1.1.
Then select File|Import|"Vitis project exported zip file". Select the archive file, and select all projects in the archive.
- Note: To have Vitis version 2024.1.1, you must install "Vivado™ Edition Update 1 - 2024.1 Product Update" on top of "Vivado™ Edition - 2024.1 Full Product Installation". See the Xilinx download page.
The following project exports are for building the demo application in Vitis 2024.1.1 (a.k.a. Vitis Unified 2024):
XADC_tutorial_timer_hw_2024.1.1.xpr.zip
- Contains the HW design project export from Vivado 2024.1.1.
- This is the HW design for the Digilent Cora Z7-07S board, which we created in the chapter Hardware design in Vivado. The only addition is the enablement of Timer 0 in the Zynq PS configuration.
Vitis Unified requires a HW timer to be present in the HW design. Otherwise, a FreeRTOS application can't be built in Vitis Unified.
vitis_archive_Unified_2024.1.1.zip
- Contains the SW project export from Vitis 2024.1.1.
- This is the XADC demo application for the Digilent Cora Z7-07S board, which I described in the chapter Software.
- To use the export, create an empty folder on your PC and open it as a workspace in Vitis 2024.1.1.
Then select File|Import. Select the archive file, and select all projects in the archive.
Comments
Please log in or sign up to comment.