Since I covered the detail behind streaming ADC samples from Digilent's Digitizer Zmod into DDR memory using S2MM transfers via AXI DMA, I figured it'd be worth taking the time to show how DAC code values can be streamed from DDR memory to the DAC on the AWG Zmod using MM2S transfers via AXI DMA.
For those of you that read that my previous post about streaming ADCs samples with S2MM transfers to write them into memory remember the fickleness I stumbled upon with getting the S2MM transfer operation to behave properly. Thankfully, I found that the MM2S side of the AXI DMA for reading samples out of memory seems to be much more easy going.
Again, I'm using Vivado/Vitis 2022.1 in this post with the corresponding Digilent Vivado library for the Zmod IP in the block design, and I'm using the AWG Zmod on SYZYGY port B of the Eclypse Z7. If you don't know how to add the IP repository for the Digilent Zmod IP, I cover those steps here.
Vivado Block DesignOverall, I'm simplifying the block design here such that it only contains the functionality for streaming DAC codes to the AWG. While this design runs on its own, it can also be basically copy+pasted into a larger design when needed.
Start by adding the Zynq Processing System IP to the block design first and run the Block Automation that appears to apply the Eclypse Z7 board presets. Then open the Zynq PS's configuration to enable one of the High Performance AXI slave ports for the AXI DMA to access the DDR with.
Add the AXI DMA with only the read channel enabled, disable the scatter gather engine, and check option to allow unaligned transfers:
Run the resultant connection automation that appears after adding and configuring the AXI DMA IP. The option for connection automation usually appears twice for the AXI DMA IP, once for the regular AXI control interface(s) via the general purpose (GP) AXI on the Zynq and again for the AXI Stream interface(s) to via the high performance (HP) AXI on the Zynq.
The AWG Zmod requires two 100MHz clocks that are 90 degrees out of phase with each other (you can read the details as to why here). The PL fabric clocks can't be configured with different phases from the Zynq PS IP so a Clocking Wizard IP is required. However, be sure to source both the 0 degree phase 100MHz clock and 90 degree phase clock from the Clocking Wizard.
It might be tempting to source the 0 degree phase 100MHz from one of the PL fabric clocks of the Zynq, but I found that it doesn't keep the 90 degree difference tight enough to be reliable and it's easy for the AWG Zmod logic to get into a metastable state. Only use the the PL fabric clock of the Zynq to feed the input of the clocking wizard to generate the two orthogonal 100MHz clocks.
Under Clocking Options, configure the Clocking Wizard primary input clock clk_in1
to have No buffer. This is important so that Vivado doesn't try to refine the PL fabric clock as a new clock (again, learned this one the hard way).
Under Output Clocks, enable clk_out1
and clk_out2
at 100MHz and set the phase for clk_out2
to 90. Also change the Reset Type to Active Low to match the reset of the system.
Next, add the AWG Zmod Controller IP. I did not enable any of the external ports, but they can be added as desired.
One more thing is needed before we start connecting everything, and that is a FIFO to handle the clock domain crossing between the AXI DMA and the AWG Zmod Controller. Because even though both are controlled by 100MHz clocks, they are separate clocks (the one controlling the AXI DMA is the PL fabric clock from the Zynq while the clock controlling the AWG Zmod is output clocks from the Clocking Wizard).
Add an AXI Stream FIFO and open its configuration window. Choose the FIFO depth according to your application, I chose the shallowest option since I'm going to just be sending single codes at a time to test/verify the analog output from the AWG Zmod's DAC. The option for Independent clocks must be enabled so the master and slave AXI ports can be connected to different clock sources matching their data (the Zynq PL clock feeding the AXI DMA and the clk_out1
from the Clocking Wizard respectively).
Connect clk_out1
from the Clocking Wizard to SysClk100
and DAC_inIO_Clk
of the Zmod AWG Controller IP, as well as m_axis_aclk
of the AXIS Data FIFO. Connect clk_out2
from the Clocking Wizard to DAC_Clk
of the Zmod AWG Controller IP. Everything else will connect to the Zynq PL fabric clock, FCLK_CLK0
, via connection automation.
After some trial and error, I found that it was best to have the AXIS Data FIFO be reset when the AXI DMA read channel (MM2S) is reset from the C code in Vitis. So I went back and added an AND gate using a Vector Utility Logic IP to AND the reset for the slave AXIS interface of the FIFO (s_axis_aresetn
) and the MM2S reset output from the AXI DMA IP (mm2s_prmry_reset_out_n
).
Final block design should look similar to this (I also have ILAs in the screenshot diagram below, but they are not integral to the function of the design).
Once the block diagram is complete, validate and save it then create an HDL wrapper. Add the constraints file for the AWG Zmod for SYZYGY port B I created and attached below and run synthesis, implementation, and generate a bitstream. Be sure the block design name matches yours in lines 45 & 46 of the constraints file for the DAC clock constraints.
create_generated_clock -name ZmodDAC_ClkIn -source [get_pins <block design name>/ZmodAWGController_0/U0/InstDAC_ClkinODDR/C] -divide_by 1 [get_ports ZmodDAC_ClkIn]
create_generated_clock -name ZmodDAC_ClkIO -source [get_pins <block design name>/ZmodAWGController_0/U0/InstDAC_ClkIO_ODDR/C] -divide_by 1 [get_ports ZmodDAC_ClkIO]
Vitis C CodeWith the hardware design complete and exported, launch into a blank Vitis workspace and create a new Platform Project with the exported XSA hardware definition. Then create a new standalone OS (aka bare metal) application project using either the Hello World or Blank application template.
As I mentioned before the MM2S transfer process is the same as the S2MM, but less fickle. I found that the following order of operations without any extra fluff worked just fine:
- Set the run bit (bit 0 in the AXI DMA MM2S control register - address 0x00)
- Write the address of the source data (bit 0 in the AXI DMA MM2S source address register - address 0x18)
- Write the transfer length in bytes (bits 0 - 25 in the AXI DMA MM2S transfer length register - address 0x28) to kick off the transaction.
You can find specifically how I coded up the above sequence in the XAxiDma_MM2Stransfer()
function in mm2s_controller.c
I have attached below.
I did find that it is import to clear the cache of the pointer to the memory that I'm writing DAC code values to between every transfer from the main function, otherwise the updated value isn't actually picked up and sent out:
Xil_DCacheFlushRange((UINTPTR)TxBufferPtr, MAX_PKT_LEN);
For the voltage to DAC code conversion algorithm, it's pretty straight forward:
DAC code = (Vout/Vref)*((2^number of DAC resolution bits)-1);
Where Vout is the desired analog output voltage, Vref is the supply voltage being fed to the DAC, and the number of DAC resolution bits is how many bits the digital input of the DAC is.
The AWG Zmod has a 14-bit DAC, however it is expecting a 2's compliment input so the MSB is reserved as the sign bit and you only get the lower 13 bits to send the actual digital value across. Therefore the equation for the DAC of the AWG Zmod becomes:
DAC code = (Vout/Vref)*((2^13)-1);
The 2^13 value must have 1 subtracted from it since everything is 0 indexed here.
The supply voltage (Vref) for the DAC on the AWG Zmod can be set to either 1.25v (low gain setting) or 5.0v (high gain setting) in the Gain Settings tab of the AWG Zmod Controller IP in Vivado.
Since the desired output voltage a numerical value with significant figures to the right of the decimal point as well, the initial data type will be double
. And the above equation is calculated with all constants and variables in the double data type in order to get the correct decimal DAC code value.
double Vout = 0.5; //desired Vout is 0.5v
double Vref = 1.25; //AWG Zmod is in low gain mode, so Vref = 1.25v
double DAC_code_double;
DAC_code_double = (Vout/Vref)*(8191);
Once the decimal DAC code value is calculated, check if it is negative. If it is negative, multiply it by -1 to make it positive again before converting it to u16. According to the AWG Zmod Controller User Guide, the 14-bit value is expected to be in bits 31-18 for channel 1 or 15-2 for channel 2. This means the value in the new u16 variable needs to be shifted left by 2. Finally, if the original DAC code value was negative, perform a bitwise OR on the u16 variable with 0x8000 to flip the sign bit to 1.
u16 DAC_code_u16;
u16 DAC_code_shifted;
u16 neg_sign = 0x8000;
if(DAC_code_double < 0){ //if desired Vout is negative, set the sign bit
DAC_code_u16 = (DAC_code_double * -1);
DAC_code_shifted = DAC_code_u16 << 2; //shift the 13-bit value up per AWG UG
DAC_code_shifted = ~DAC_code_shifted;
DAC_code_shifted = DAC_code_shifted | neg_sign; //set the sign bit
} else {
DAC_code_u16 = DAC_code_double; //shift the 12-bit value up per AWG UG
DAC_code_shifted = DAC_code_u16 << 2;
}
Finally, since the buffer is loaded one byte (u8) at a time, the final u16 variable can be loaded into two u8 variables:
u8 upper_byte = DAC_code >> 8; //shift upper 8 bits of u16 into new u8 var
u8 lower_byte = DAC_code; //place lower 8 bits of u16 into new u8 var
TxBufferPtr[3] = upper_byte; //DAC ch1
TxBufferPtr[2] = lower_byte; //DAC ch1
TxBufferPtr[1] = upper_byte; //DAC ch2
TxBufferPtr[0] = lower_byte; //DAC ch2
Altogether:
double Vout = 0.5; //desired Vout is 0.5v
double Vref = 1.25; //AWG Zmod is in low gain mode, so Vref = 1.25v
double DAC_code_double;
u16 DAC_code_u16;
u16 DAC_code;
u16 neg_sign = 0x8000;
DAC_code_double = (Vout/Vref)*(8191);
if(DAC_code_double < 0){ //if desired Vout is negative, set the sign bit
DAC_code_u16 = (DAC_code_double * -1);
DAC_code = DAC_code_u16 << 2; //shift the 13-bit value up per AWG UG
DAC_code = ~DAC_code; // invert for 2's compliment
DAC_code = DAC_code | neg_sign; //set the sign bit
} else {
DAC_code_u16 = DAC_code_double; //shift the 12-bit value up per AWG UG
DAC_code = DAC_code_u16 << 2;
}
u8 upper_byte = DAC_code >> 8; //shift upper 8 bits of u16 into new u8 var
u8 lower_byte = DAC_code; //place lower 8 bits of u16 into new u8 var
TxBufferPtr[3] = upper_byte; //DAC ch1
TxBufferPtr[2] = lower_byte; //DAC ch1
TxBufferPtr[1] = upper_byte; //DAC ch2
TxBufferPtr[0] = lower_byte; //DAC ch2
Keep in mind that I have not accounted for any calibration offsets in my equation. My AWG Zmod was pretty spot on out of the box. But calibration is pretty easy. Just tell the DAC to output 0v (DAC code = 00000000000000) and measure the analog output. Whatever non-zero output you measure is factored into the DAC code equation as either an additive (CA) or multiplicative constant (CG):
DAC code = ((Vout-CA)/((1+CG)*Vref))*((2^13)-1);
Debugging in VitisIn order to test my MM2S code and my DAC code calculation algorithm, I hooked one of my DAC channels to my USB scope and ran the application as a Single Application Debug on Hardware.
Launch Single Application Debug on Hardware by right-clicking on the application name from the Explorer window and selecting Debug As.
I high recommend adding memory monitors to the buffer the DAC codes are being written to and the DMA is subsequently reading from (this is how I discovered I needed to clear the buffer cache between transfers after updating the values in those locations).
The ILAs in Vivado can be opened via the Hardware Manager after the Debug run has been launched and the breakpoint at the first line in main is hit. Setting the trigger on tvalid of the MM2S interface is the best way to capture the transfer.
I stepped through the C code to test every analog output possible for the DAC (-1.25v to +1.25v in the low gain setting) to verify my AWG Zmod so now I'm ready to start deploying it in larger designs!
Comments
Please log in or sign up to comment.