Recently I've been working with with Eclypse Z7 with the Digitizer and AWG Zmods. Now that I've gotten base design working for each Zmod separately, I wanted to see how I could implement them as a system for projects. To achieve this, I needed to decide on data processing tasks for the data samples coming from the ADC on the Digitizer and the data samples that are being converted to DAC codes being sent to the AWG Zmod.
For the AWG Zmod, I wanted to implement a way to generate a sine wave since this is a primary function of an AWG (Analog Waveform Generator). The simplest way to do this is to write C code to calculate the magnitude value of a sine function for each of the 360 degrees of the unit circle (this feels like an awkward sentence, not sure why but hopefully I'm making sense). The only constraint here will be timing and how fast I can calculate each magnitude value in the C code which will translate to the maximum frequency sine wave I can output from the AWG. I'll explain this in more detail later.
For the Digitizer Zmod, I also went along the same lines of thinking of what the most common use cases might be. Assuming some sort of sinusoid is being input to the Digitizer Zmod, a very common function might be to filter it. So I decided to pass the signal coming from the Digtizer's ADC through a low pass filter (LPF) using the FIR Compiler IP in the block design.
This ultimately lead to the idea of creating a "FIR Tester" project with the AWG Zmod's output looped back to the Digitizer Zmod's input to feed it a variable frequency sine wave to test/validate the FIR's stopband and passband with a real-world signal.
Note: I'm using Vivado/Vitis 2022.1 in this post, but the general concepts explained should be version agnostic.
Digitizer Zmod Hardware DesignIn a previous post, I already detailed the design for streaming ADC data samples into DDR memory of the Eclypse board from the Digitizer Zmod, so I just needed to add the FIR compiler IP into the data path.
Since the FIR Compiler doesn't require the tlast
signal to operate appropriately, I placed it in-between the Zmod Digitizer Controller IP output that is streaming ADC samples, and the AXI-Stream Data FIFO that's feeding my custom Data Packetizer module. The Data Packetizer module I created to generates the tlast
signal at the ends of a S2MM transfer that the AXI DMA IP requires.
Since I'm using AMD's FIR Compiler IP, the only thing I needed to provide it were the coefficients for the LPF I wanted to implement. I found this neat website that lets you input the desired passband/stopband frequencies with sample rate and it generates the filter's coefficients for you.
I chose to implement an LPF with a sample rate of 100kHz, a passband of 0Hz - 4kHz, and a stopband of 8kHz - 50kHz. I chose this range because I found that the highest frequency sine wave I could generate from the C code was 12kHz (Just to reiterate: this is a limitation due to timing in my C code implementation, not the AWG Zmod's hardware).
I simply copy+pasted the plain text coefficient values from the TFilter website into the Coefficient Vector variable in the first tab of the FIR Compiler's configuration window. Then I set my sample rate of 100kHz in the second tab since it defaults to the clock speed of the AXI Stream slave interface (which is 100MHz in my design).
I also increased the S2MM transfer length of my Data Packetizer module so I wouldn't have to trigger more transfers from the C code as I wanted a larger data set to send through the FIR at one time.
I also had to tweak the behavior/timing for asserting tlast
to uphold the "unspoken" rules of AXI DMA I mention in my previous post as I discovered that the AXI Stream behavior from each IP is a bit different and adding the FIR Compiler to the pipeline changed the timing.
Finally I added an ILA to the output (master) port of the FIR Compiler so that I could see if it was working as expected without writing extra C code to process it from the DDR.
I also recently did a project writeup detailing how to stream DAC code values to the AWG Zmod from DDR memory on the Eclpyse board. Since I'm confining the sine wave magnitude calculation to the C code, I copied the hardware design from that project straight to this one. The only difference being that it shares the same AXI DMA IP as the Digitizer Zmod so both the DMA's read and write channels are enabled.
Since I kept the Digitizer Zmod on SYZYGY port A and the AWG Zmod on SYZYGY port B, I was able to reuse the constraints files from the two respective project for each to generate a bitstream in Vivado and export the hardware package XSA file to use in Vitis.
As usual, I created a new Vitis workspace then created a new Platform Project targeting the exported XSA from Vivado. I then created a new Application Project using the Hello World template. For the most part, I was able to copy the C code from my previous posts for the S2MM transfer from the Zmod Digitizer and the MM2S transfer to the AWG Zmod.
Utilizing the same enable GPIO lines to make sure ADC samples aren't streamed to the DMA engine before it's setup and ready to receive them:
Status = XAxiDma_S2MMtransfer(&AxiDma,(UINTPTR) RxBufferPtr, MAX_PKT_LEN);
EnaAcqDigitizer(GPIO_DIGITAL_HIGH);
EnaDigitizerDataFIFO(GPIO_DIGITAL_HIGH);
And ensuring to clear the cache of the buffer for the Tx Buffer pointer before every MM2S transfer. I found this was critical to ensure the updated values were what was actually transferred to memory from the pointer:
Xil_DCacheFlushRange((UINTPTR)TxBufferPtr, MAX_PKT_LEN);
Status = XAxiDma_MM2Stransfer(&AxiDma,(UINTPTR) TxBufferPtr, MAX_PKT_LEN);
To created the sine wave, I used the sin()
function to calculate the magnitude value for each of the 360 degrees of the unit circle. This magnitude value is the voltage value the DAC will output that is passed to the conversion function to calculate the relative digital DAC code to transfer to the AWG Zmod.
The sin()
function does expect the input argument to be in radians, so a simple conversion from degrees to radians. It also requires the math flag (-m) to be set in the C/C++ compiler in Vitis for the application, otherwise the build will fail.
Right-click on the application name from the Explorer window and select C/C++ Build Settings. Then under ARM v7 gcc linker > Libraries, add "m" as a Library:
Code for generating the sine wave using the sin()
function:
u8 upper_mask, lower_mask;
u16 DAC_code = 0;
double Vout = 0;
double degree_convert = M_PI/180;
for(double i=0;i<361;i=i+1){
Vout = sin(i*degree_convert);
DAC_code = voltage_to_DACcodeLow(Vout);
u8 upper_mask = DAC_code >> 8;
u8 lower_mask = DAC_code;
TxBufferPtr[3] = upper_mask; //ch1
TxBufferPtr[2] = lower_mask; //ch1
TxBufferPtr[1] = upper_mask; //ch2
TxBufferPtr[0] = lower_mask; //ch2
Xil_DCacheFlushRange((UINTPTR)RxBufferPtr, MAX_PKT_LEN);
Xil_DCacheFlushRange((UINTPTR)TxBufferPtr, MAX_PKT_LEN);
XAxiDma_Reset(&AxiDma);
Status = XAxiDma_S2MMtransfer(&AxiDma,(UINTPTR) RxBufferPtr, MAX_PKT_LEN);
if (Status != XST_SUCCESS){
xil_printf("XAXIDMA_DEVICE_TO_DMA transfer failed...\r\n");
return XST_FAILURE;
}
Status = XAxiDma_MM2Stransfer(&AxiDma,(UINTPTR) TxBufferPtr, MAX_PKT_LEN);
if (Status != XST_SUCCESS){
xil_printf("XAXIDMA_DMA_TO_DEVICE transfer failed...\r\n");
return XST_FAILURE;
}
}
Then to create the chirp, I simply wrapped the for loop
that creates one period of a sine wave (ie - one revolution around the unit circle, calculating the magnitude of sin()
for 360 degrees) within another for loop
that inserts a delay between the each DAC code sent to the AWG which therefore changes the period/frequency of the sine wave the AWG outputs. The for loop
starts with the longest delay inserted so the chirp starts at the lowest frequency, then it decrements the delay so the chirp increases in frequency, with one period of each frequency being output:
u8 upper_mask, lower_mask;
u16 DAC_code = 0;
double Vout = 0;
ULONG useconds = 1000;
for(double j=100;j>-1;j--){
for(double i=0;i<361;i=i+1){
Vout = sin(i*degree_convert);
DAC_code = voltage_to_DACcodeLow(Vout);
u8 upper_mask = DAC_code >> 8;
u8 lower_mask = DAC_code;
TxBufferPtr[3] = upper_mask; //ch1
TxBufferPtr[2] = lower_mask; //ch1
TxBufferPtr[1] = upper_mask; //ch2
TxBufferPtr[0] = lower_mask; //ch2
Xil_DCacheFlushRange((UINTPTR)RxBufferPtr, MAX_PKT_LEN);
Xil_DCacheFlushRange((UINTPTR)TxBufferPtr, MAX_PKT_LEN);
XAxiDma_Reset(&AxiDma);
Status = XAxiDma_S2MMtransfer(&AxiDma,(UINTPTR) RxBufferPtr, MAX_PKT_LEN);
if (Status != XST_SUCCESS){
xil_printf("XAXIDMA_DEVICE_TO_DMA transfer failed...\r\n");
return XST_FAILURE;
}
Status = XAxiDma_MM2Stransfer(&AxiDma,(UINTPTR) TxBufferPtr, MAX_PKT_LEN);
if (Status != XST_SUCCESS){
xil_printf("XAXIDMA_DMA_TO_DEVICE transfer failed...\r\n");
return XST_FAILURE;
}
usleep(useconds); // this delay controls the period of the sine wave
}
useconds = useconds - 10; // decrement in 10us increments
}
The second for loop
with j
for its index starts with a delay of 1000us (1ms) which results in a 923Hz sine wave being output from the AWG Zmod, and ends with a delay of 0us which results in a 12kHz sine wave being output from the AWG Zmod. So the resultant chirp signal starts well within the target LPF's passband, and ends well into its stopband so we can see how well it performs.
To explain how I calculate the above values: I found that 12kHz (a period of 83.25us) was the fastest the DAC codes could be streamed from the C code from memory without any optimization. Since a 0us delay in the C code resulted in a 83.25us period sine wave, I used that as my reference and added any delay to that. So for example: 83.25us + 1000us delay = 1083.25us, 1/1083.25us = 923Hz.
I measured the base sine wave with no added delay using the scope function on my ADP5250 (an ADP3450 or AD3/AD2 will work as well) to measure the amount of time on the x-axis it took to see the full sine wave without any delay inserted:
Obviously, this is not the maximum frequency that the Eclypse FPGA board and AWG Zmod can provide, it just happened to be the maximum of my quick & easy way of generating a sine wave in C code I could think of off the top of my head.
See the attached C files below for the full application to use in your own Vitis workspace.
ResultsTo test the system I launched a debug run for application in Vitis and then opened the Hardware Manager in Vivado to connect to the ILAs right after Vitis hit the first automatic breakpoint right inside the main()
function.
I set triggers in the ILA on the slave tvalid
signal of the FIR and tlast
on my Data Packetizer module to make sure the transfer was correct. Then I switched back to Vitis to run the application.
And the output for the FIR is successfully attenuating!
Comments
Please log in or sign up to comment.