This project is a continuation of my full detailed bring up of the Eclypse Z7 with ADC & DAC ZMODs. As I had mentioned in that project, I am adding onto the design by integrating a DDS Complier IP block into the block design and using that to generate the digital 1 MHz sine wave data for the DAC ZMOD to output on one of its channels. To verify the data, it'll be read in on one of the channels of the ADC ZMOD. I will also hook it up to my signal analyzer to see what the physical 1MHz wave looks like.
To start I hooked up channel one of the DAC ZMOD to channel one of the ADC ZMOD:
Just as a side note for reference, I am again using Vivado and Vitis version 2019.2 and I'm using the exact same project I covered how to create in detail in my last project post on the Eclypse Z7.
Starting with the existing hardware design in Vivado, the first thing that needs to be modified is the block design. I'm a big fan of using a DDS compiler for generating a sine wave because they are the best tradeoff between fabric resource utilization and accuracy of the output wave.
Open the block design and add a DDS compiler to IP block design, double-click on it to open its customization window.
One of the biggest advantages of using the DDS compiler IP is the smooth/seamless transition when changing the frequency/phase of the output signal (so you don't have to worry about phase discontinuities). This is why I like to use the streaming option for the programmability of the output frequency/phase.
I chose to only focus on changing the output frequency for now (via the phase increment programability) and set the phase offset programmability to none:
For the AXI stream protocol with the DMA to interface with the DAC ZMOD to write the sine wave out, the stream needs to be packetized, output tready, and assert tlast at the end of each period of the sine wave. Select the option for 'Packet Framing' under Data has TLAST, and check the box for 'Output TREADY'.
The DDS compiler will write its output to the Eclypse's DDR memory for the DAC to read from via the DMA engine. To make integration as straightforward as possible, I simply enabled the write channel for the DMA engine for the DDS's output. Since the data is streaming one period of sinusoid at a time, the DRE (data realignment engine) also needs to be enabled for the write channel. Check the box to 'Allow Unaligned Transfers' for the write channel (only for the write channel though, as the bare-metal Zmod libraries take care of aligning the data for transfers on the read channel).
Manually connect the M_AXIS_DATA output from the DDS Compiler to the S_AXIS_S2MM of the DAC's DMA engine. The tkeep signal of the DMA's S_AXIS_S2MM port needs to be held high across its bus to signal that all incoming data is valid (including any zero value data bytes). To do this, add a constant IP block to the design, set the output width to match that of the tkeep of the DMA's S_AXIS_S2MM port and set the value to be high across the bus (in this case, the tkeep signal is two bits wide, so the constant value would be set to 3, which is 2'b11). Then manually connect its output to the DMA's S_AXIS_S2MM port.
I decided it would be easier to control the input of the DDS compiler from the programmable logic of the Zynq chip. In order to do this the phase input port of the DDS compiler needs to be externally available of the block diagram which is done by right-clicking on the port name and selecting the option to "Make external". You'll see a port appear of the AXI stream bus for the DDS compiler's input.
The FCLK_CLK1 clock from the Zynq processing system also needs to be brought out to a port in the block design so it is available to the HDL in the programmable logic of the Zynq chip. Simply right-click on the FCLK_CLK1 port name on the Zynq processing system IP block and select 'Create Port...':
The block design should now look similar to this:
To add the logic analyzer for debugging later, mark the DDS's phase input, along with the DDS data output, the AXIS_MM2S of the DAC's DMA and the AXIS_S2MM of the ADC's DMA for debugging. Just right-click on each line and select 'Debug':
After a few moments, the option for connection automation will appear. Select it and be sure to check the box for the option for the AXI protocol checker.
Once connection automation has completed its run, validate the block design to check for any errors or critical warnings, then save & close it.
A new HDL instantiation needs to be created for the block design since new external ports have been added to it. This can be done by simply right-clicking on the block design file in the Sources Hierarchy tab and selecting the option to 'Create HDL Wrapper...'. It will simply update the existing one with the new block diagram instantiation (just like in the last project, select the option to let Vivado auto-update and manage it).
However, since none of the new external ports will be routed to actual package pins on the Zynq chip and other custom HDL also needs to be added, a new top level file needs to be created from scratch. Even thought there is an option to allow the user to manage the wrapper in the last step, I've found that with Vivado its better to always select the auto-manage option then create your own top level file from scratch and simply copy+paste the block diagram instantiation from the auto-generated wrapper.
For this design I am creating three design sources of my own: the new custom top level file, the logic for generating the phase increment value to send to the DDS compiler, and the state machine for the AXI stream protocol to communicate with the DDS compiler.
Select the Add Sources option from the Flow Navigator and choose Add or create design sources in the pop up window. The window will then give you the option to create how ever many files you need (three Verilog module files in this case).
I named the top file 'eclypse_top', the phase increment logic file 'bb_logic', and the AXI stream state machine file 'axis_sm'.
The top file will be mostly a copy + paste from the design_wrapper file autogenerated for the block design instantiation. It will also instantiate the phase increment logic module. Note that the slave AXI stream signals for the DDS compiler and FCLK_CLK1 are commented out from the module port specifier as they are being redirected to the phase increment logic module instead of being routed to a package pin on the Zynq chip. The phase increment logic module is then responsible for instantiating the AXI stream state machine module.
Custom top file Verilog:
module eclypse_top(
inout [14:0]DDR_addr,
inout [2:0]DDR_ba,
inout DDR_cas_n,
inout DDR_ck_n,
inout DDR_ck_p,
inout DDR_cke,
inout DDR_cs_n,
inout [3:0]DDR_dm,
inout [31:0]DDR_dq,
inout [3:0]DDR_dqs_n,
inout [3:0]DDR_dqs_p,
inout DDR_odt,
inout DDR_ras_n,
inout DDR_reset_n,
inout DDR_we_n,
// input [31:0]DDS_S_AXIS_PHASE_tdata,
// input DDS_S_AXIS_PHASE_tlast,
// output DDS_S_AXIS_PHASE_tready,
// input DDS_S_AXIS_PHASE_tvalid,
input DcoClk_0,
// output FCLK_CLK1,
inout FIXED_IO_ddr_vrn,
inout FIXED_IO_ddr_vrp,
inout [53:0]FIXED_IO_mio,
inout FIXED_IO_ps_clk,
inout FIXED_IO_ps_porb,
inout FIXED_IO_ps_srstb,
output adcClkIn_n_0,
output adcClkIn_p_0,
output adcSync_0,
input [1:0]btn_2bits_tri_i,
input [13:0]dADC_Data_0,
inout pmod_ja_pin10_io,
inout pmod_ja_pin1_io,
inout pmod_ja_pin2_io,
inout pmod_ja_pin3_io,
inout pmod_ja_pin4_io,
inout pmod_ja_pin7_io,
inout pmod_ja_pin8_io,
inout pmod_ja_pin9_io,
inout pmod_jb_pin10_io,
inout pmod_jb_pin1_io,
inout pmod_jb_pin2_io,
inout pmod_jb_pin3_io,
inout pmod_jb_pin4_io,
inout pmod_jb_pin7_io,
inout pmod_jb_pin8_io,
inout pmod_jb_pin9_io,
output [5:0]rgbled_6bits_tri_o,
output sADC_CS_0,
inout sADC_SDIO_0,
output sADC_Sclk_0,
output sCh1CouplingH_0,
output sCh1CouplingL_0,
output sCh1GainH_0,
output sCh1GainL_0,
output sCh2CouplingH_0,
output sCh2CouplingL_0,
output sCh2GainH_0,
output sCh2GainL_0,
output sDAC_CS_0,
output sDAC_ClkIO_0,
output sDAC_Clkin_0,
output [13:0]sDAC_Data_0,
output sDAC_EnOut_0,
output sDAC_Reset_0,
output sDAC_SCLK_0,
inout sDAC_SDIO_0,
output sDAC_SetFS1_0,
output sDAC_SetFS2_0,
output sRelayComH_0,
output sRelayComL_0,
input sys_clock
);
wire FCLK_CLK1;
wire [31:0]DDS_S_AXIS_PHASE_tdata;
wire DDS_S_AXIS_PHASE_tlast;
wire DDS_S_AXIS_PHASE_tready;
wire DDS_S_AXIS_PHASE_tvalid;
// block diagram instantiation
design_1 design_1_i(
.DDR_addr(DDR_addr),
.DDR_ba(DDR_ba),
.DDR_cas_n(DDR_cas_n),
.DDR_ck_n(DDR_ck_n),
.DDR_ck_p(DDR_ck_p),
.DDR_cke(DDR_cke),
.DDR_cs_n(DDR_cs_n),
.DDR_dm(DDR_dm),
.DDR_dq(DDR_dq),
.DDR_dqs_n(DDR_dqs_n),
.DDR_dqs_p(DDR_dqs_p),
.DDR_odt(DDR_odt),
.DDR_ras_n(DDR_ras_n),
.DDR_reset_n(DDR_reset_n),
.DDR_we_n(DDR_we_n),
.DDS_S_AXIS_PHASE_tdata(DDS_S_AXIS_PHASE_tdata),
.DDS_S_AXIS_PHASE_tlast(DDS_S_AXIS_PHASE_tlast),
.DDS_S_AXIS_PHASE_tready(DDS_S_AXIS_PHASE_tready),
.DDS_S_AXIS_PHASE_tvalid(DDS_S_AXIS_PHASE_tvalid),
.DcoClk_0(DcoClk_0),
.FCLK_CLK1(FCLK_CLK1),
.FIXED_IO_ddr_vrn(FIXED_IO_ddr_vrn),
.FIXED_IO_ddr_vrp(FIXED_IO_ddr_vrp),
.FIXED_IO_mio(FIXED_IO_mio),
.FIXED_IO_ps_clk(FIXED_IO_ps_clk),
.FIXED_IO_ps_porb(FIXED_IO_ps_porb),
.FIXED_IO_ps_srstb(FIXED_IO_ps_srstb),
.adcClkIn_n_0(adcClkIn_n_0),
.adcClkIn_p_0(adcClkIn_p_0),
.adcSync_0(adcSync_0),
.btn_2bits_tri_i(btn_2bits_tri_i),
.dADC_Data_0(dADC_Data_0),
.pmod_ja_pin10_i(pmod_ja_pin10_i),
.pmod_ja_pin10_o(pmod_ja_pin10_o),
.pmod_ja_pin10_t(pmod_ja_pin10_t),
.pmod_ja_pin1_i(pmod_ja_pin1_i),
.pmod_ja_pin1_o(pmod_ja_pin1_o),
.pmod_ja_pin1_t(pmod_ja_pin1_t),
.pmod_ja_pin2_i(pmod_ja_pin2_i),
.pmod_ja_pin2_o(pmod_ja_pin2_o),
.pmod_ja_pin2_t(pmod_ja_pin2_t),
.pmod_ja_pin3_i(pmod_ja_pin3_i),
.pmod_ja_pin3_o(pmod_ja_pin3_o),
.pmod_ja_pin3_t(pmod_ja_pin3_t),
.pmod_ja_pin4_i(pmod_ja_pin4_i),
.pmod_ja_pin4_o(pmod_ja_pin4_o),
.pmod_ja_pin4_t(pmod_ja_pin4_t),
.pmod_ja_pin7_i(pmod_ja_pin7_i),
.pmod_ja_pin7_o(pmod_ja_pin7_o),
.pmod_ja_pin7_t(pmod_ja_pin7_t),
.pmod_ja_pin8_i(pmod_ja_pin8_i),
.pmod_ja_pin8_o(pmod_ja_pin8_o),
.pmod_ja_pin8_t(pmod_ja_pin8_t),
.pmod_ja_pin9_i(pmod_ja_pin9_i),
.pmod_ja_pin9_o(pmod_ja_pin9_o),
.pmod_ja_pin9_t(pmod_ja_pin9_t),
.pmod_jb_pin10_i(pmod_jb_pin10_i),
.pmod_jb_pin10_o(pmod_jb_pin10_o),
.pmod_jb_pin10_t(pmod_jb_pin10_t),
.pmod_jb_pin1_i(pmod_jb_pin1_i),
.pmod_jb_pin1_o(pmod_jb_pin1_o),
.pmod_jb_pin1_t(pmod_jb_pin1_t),
.pmod_jb_pin2_i(pmod_jb_pin2_i),
.pmod_jb_pin2_o(pmod_jb_pin2_o),
.pmod_jb_pin2_t(pmod_jb_pin2_t),
.pmod_jb_pin3_i(pmod_jb_pin3_i),
.pmod_jb_pin3_o(pmod_jb_pin3_o),
.pmod_jb_pin3_t(pmod_jb_pin3_t),
.pmod_jb_pin4_i(pmod_jb_pin4_i),
.pmod_jb_pin4_o(pmod_jb_pin4_o),
.pmod_jb_pin4_t(pmod_jb_pin4_t),
.pmod_jb_pin7_i(pmod_jb_pin7_i),
.pmod_jb_pin7_o(pmod_jb_pin7_o),
.pmod_jb_pin7_t(pmod_jb_pin7_t),
.pmod_jb_pin8_i(pmod_jb_pin8_i),
.pmod_jb_pin8_o(pmod_jb_pin8_o),
.pmod_jb_pin8_t(pmod_jb_pin8_t),
.pmod_jb_pin9_i(pmod_jb_pin9_i),
.pmod_jb_pin9_o(pmod_jb_pin9_o),
.pmod_jb_pin9_t(pmod_jb_pin9_t),
.rgbled_6bits_tri_o(rgbled_6bits_tri_o),
.sADC_CS_0(sADC_CS_0),
.sADC_SDIO_0(sADC_SDIO_0),
.sADC_Sclk_0(sADC_Sclk_0),
.sCh1CouplingH_0(sCh1CouplingH_0),
.sCh1CouplingL_0(sCh1CouplingL_0),
.sCh1GainH_0(sCh1GainH_0),
.sCh1GainL_0(sCh1GainL_0),
.sCh2CouplingH_0(sCh2CouplingH_0),
.sCh2CouplingL_0(sCh2CouplingL_0),
.sCh2GainH_0(sCh2GainH_0),
.sCh2GainL_0(sCh2GainL_0),
.sDAC_CS_0(sDAC_CS_0),
.sDAC_ClkIO_0(sDAC_ClkIO_0),
.sDAC_Clkin_0(sDAC_Clkin_0),
.sDAC_Data_0(sDAC_Data_0),
.sDAC_EnOut_0(sDAC_EnOut_0),
.sDAC_Reset_0(sDAC_Reset_0),
.sDAC_SCLK_0(sDAC_SCLK_0),
.sDAC_SDIO_0(sDAC_SDIO_0),
.sDAC_SetFS1_0(sDAC_SetFS1_0),
.sDAC_SetFS2_0(sDAC_SetFS2_0),
.sRelayComH_0(sRelayComH_0),
.sRelayComL_0(sRelayComL_0),
.sys_clock(sys_clock));
IOBUF pmod_ja_pin10_iobuf(
.I(pmod_ja_pin10_o),
.IO(pmod_ja_pin10_io),
.O(pmod_ja_pin10_i),
.T(pmod_ja_pin10_t));
IOBUF pmod_ja_pin1_iobuf(
.I(pmod_ja_pin1_o),
.IO(pmod_ja_pin1_io),
.O(pmod_ja_pin1_i),
.T(pmod_ja_pin1_t));
IOBUF pmod_ja_pin2_iobuf(
.I(pmod_ja_pin2_o),
.IO(pmod_ja_pin2_io),
.O(pmod_ja_pin2_i),
.T(pmod_ja_pin2_t));
IOBUF pmod_ja_pin3_iobuf(
.I(pmod_ja_pin3_o),
.IO(pmod_ja_pin3_io),
.O(pmod_ja_pin3_i),
.T(pmod_ja_pin3_t));
IOBUF pmod_ja_pin4_iobuf(
.I(pmod_ja_pin4_o),
.IO(pmod_ja_pin4_io),
.O(pmod_ja_pin4_i),
.T(pmod_ja_pin4_t));
IOBUF pmod_ja_pin7_iobuf(
.I(pmod_ja_pin7_o),
.IO(pmod_ja_pin7_io),
.O(pmod_ja_pin7_i),
.T(pmod_ja_pin7_t));
IOBUF pmod_ja_pin8_iobuf(
.I(pmod_ja_pin8_o),
.IO(pmod_ja_pin8_io),
.O(pmod_ja_pin8_i),
.T(pmod_ja_pin8_t));
IOBUF pmod_ja_pin9_iobuf(
.I(pmod_ja_pin9_o),
.IO(pmod_ja_pin9_io),
.O(pmod_ja_pin9_i),
.T(pmod_ja_pin9_t));
IOBUF pmod_jb_pin10_iobuf(
.I(pmod_jb_pin10_o),
.IO(pmod_jb_pin10_io),
.O(pmod_jb_pin10_i),
.T(pmod_jb_pin10_t));
IOBUF pmod_jb_pin1_iobuf(
.I(pmod_jb_pin1_o),
.IO(pmod_jb_pin1_io),
.O(pmod_jb_pin1_i),
.T(pmod_jb_pin1_t));
IOBUF pmod_jb_pin2_iobuf(
.I(pmod_jb_pin2_o),
.IO(pmod_jb_pin2_io),
.O(pmod_jb_pin2_i),
.T(pmod_jb_pin2_t));
IOBUF pmod_jb_pin3_iobuf(
.I(pmod_jb_pin3_o),
.IO(pmod_jb_pin3_io),
.O(pmod_jb_pin3_i),
.T(pmod_jb_pin3_t));
IOBUF pmod_jb_pin4_iobuf(
.I(pmod_jb_pin4_o),
.IO(pmod_jb_pin4_io),
.O(pmod_jb_pin4_i),
.T(pmod_jb_pin4_t));
IOBUF pmod_jb_pin7_iobuf(
.I(pmod_jb_pin7_o),
.IO(pmod_jb_pin7_io),
.O(pmod_jb_pin7_i),
.T(pmod_jb_pin7_t));
IOBUF pmod_jb_pin8_iobuf(
.I(pmod_jb_pin8_o),
.IO(pmod_jb_pin8_io),
.O(pmod_jb_pin8_i),
.T(pmod_jb_pin8_t));
IOBUF pmod_jb_pin9_iobuf(
.I(pmod_jb_pin9_o),
.IO(pmod_jb_pin9_io),
.O(pmod_jb_pin9_i),
.T(pmod_jb_pin9_t));
// phase increment logic module instantiation
bb_logic bb_logic_i(
.clk(FCLK_CLK1),
.DDS_S_AXIS_PHASE_tdata(DDS_S_AXIS_PHASE_tdata),
.DDS_S_AXIS_PHASE_tlast(DDS_S_AXIS_PHASE_tlast),
.DDS_S_AXIS_PHASE_tready(DDS_S_AXIS_PHASE_tready),
.DDS_S_AXIS_PHASE_tvalid(DDS_S_AXIS_PHASE_tvalid));
endmodule
For the output frequency of the DDS, I chose to hardcore it just for now to focus on making sure the DDS compiler integration in the block design is correct. Since the DAC & ADC's max sample rate is 100Ms/s, the max output frequency would be 50MHz following Nyquist. I decided to go with 1MHz as an easy number (see my initial DDS Compiler tutorial for how I calculated the hex value phase increment input for 1MHz).
Phase increment logic module file Verilog:
module bb_logic(
input clk,
output [31:0] DDS_S_AXIS_PHASE_tdata, // input to block design
output DDS_S_AXIS_PHASE_tlast, // input to block design
input DDS_S_AXIS_PHASE_tready, // output from block design
output DDS_S_AXIS_PHASE_tvalid // input to block design
);
wire [31:0] Freq;
wire [31:0] Freq_period;
// setting the phase increment value to static for now
assign Freq = 32'h28f5c2;
assign Freq_period = 32'd100; // 1000ns/10ns = 100 --> max value here is 16384
wire latch_tdata;
// AXI stream state machine instantiation
axis_sm axis_sm_i(
.clk(clk),
.reset(1'b1),
.start(1'b1),
.latch_tdata(latch_tdata),
.s_phase_tvalid(DDS_S_AXIS_PHASE_tvalid), // output
.s_phase_tlast(DDS_S_AXIS_PHASE_tlast), // output
.s_phase_tready(DDS_S_AXIS_PHASE_tready), // input
.s_phase_tdata(DDS_S_AXIS_PHASE_tdata), // output
.carrier_freq(Freq),
.carrier_period(Freq_period)
);
endmodule
AXI Stream protocol state machine:
module axis_sm(
input clk,
input reset,
input start,
output reg latch_tdata,
output reg s_phase_tvalid,
output reg s_phase_tlast,
input s_phase_tready,
output reg [31:0] s_phase_tdata,
input [31:0] carrier_freq,
input [31:0] carrier_period
);
reg [4:0] state_reg;
reg [31:0] period_wait_cnt;
parameter init = 5'd0;
parameter WaitForStart = 5'd1;
parameter SetTvalidHigh = 5'd2;
parameter SetSlavePhaseValue = 5'd3;
parameter LatchTdata = 5'd4;
parameter CheckTready = 5'd5;
parameter WaitState = 5'd6;
parameter SetTlastHigh = 5'd7;
parameter WaitOneState = 5'd8;
parameter SetTlastLow = 5'd9;
parameter set_freq = 1'b0;
parameter set_phase = 1'b1;
parameter default_tdata = 32'h0;
always @ (posedge clk or posedge reset)
begin
// Default Outputs
latch_tdata <= 1'b0;
if (reset == 1'b0)
begin
s_phase_tdata[31:0] <= default_tdata;
state_reg <= init;
end
else
begin
case(state_reg)
init : //0
begin
latch_tdata <= 1'b0;
s_phase_tlast <= 1'b0;
s_phase_tvalid <= 1'b0;
period_wait_cnt <= 32'd0;
state_reg <= WaitForStart;
end
WaitForStart : //1
begin
if (start == 1'b1)
begin
state_reg <= SetTvalidHigh;
end
else
begin
state_reg <= WaitForStart;
end
end
SetTvalidHigh : //2
begin
s_phase_tvalid <= 1'b1;
state_reg <= SetSlavePhaseValue;
end
SetSlavePhaseValue : //3
begin
s_phase_tdata[31:0] <= carrier_freq;
state_reg <= LatchTdata;
end
LatchTdata : //4
begin
latch_tdata <= 1'b1;
state_reg <= CheckTready;
end
CheckTready : //5
begin
if (s_phase_tready == 1'b1)
begin
state_reg <= WaitState;
end
else if (start == 1'b0)
begin
state_reg <= init;
end
else
begin
state_reg <= CheckTready;
end
end
WaitState : //6
begin
if (period_wait_cnt >= carrier_period)
begin
period_wait_cnt <= 32'd0;
state_reg <= SetTlastHigh;
end
else
begin
period_wait_cnt <= period_wait_cnt + 1;
state_reg <= WaitState;
end
end
SetTlastHigh : //7
begin
s_phase_tlast <= 1'b1;
state_reg <= WaitOneState;
end
WaitOneState : //8
begin
state_reg <= SetTlastLow;
end
SetTlastLow : //9
begin
s_phase_tlast <= 1'b0;
state_reg <= WaitForStart;
end
endcase
end
end
endmodule
Once the new top level file is complete along with the other two files, save all of the files and you'll see the Sources Hierarchy automatically update. Set the eclypse_top file as the new top file for the project by right-clicking on it in the Sources Hierarchy tab and selecting the option to 'Set as Top'.
With the new custom top level file in place, the auto-generated file is no longer needed for the moment. However, I've found that if the block design is updated again in the future, it's good to still have it. This along with the fact that I've learned the hard way many times that deleting files auto-generated by Vivado can lead to undefined behavior in the tool, I just disable the file instead. Right-click on it in the Sources Hierarchy tab and select 'Disable'. Once updated, your Sources Hierarchy tab should look similar to the following:
As I mentioned in my previous project, the timing for the base project was off. The hold time on the data lines going out to the DAC Zmod on the SYZYGY connector was not long enough. I didn't fix it in my previous project knowing that my additions in this project would change the place and routing of the design during implementation and ultimately change the timing to some extent. Luckily, the additions in this project added enough delay that I was able to simply extend the hold times for the DAC's data lines in the constraints file (the last four lines):
set_output_delay -clock [get_clocks sDAC_Clkin_0] -clock_fall -min -add_delay 0.330 [get_ports {sDAC_Data_0[*]}]
set_output_delay -clock [get_clocks sDAC_Clkin_0] -clock_fall -max -add_delay 0.250 [get_ports {sDAC_Data_0[*]}]
set_output_delay -clock [get_clocks sDAC_Clkin_0] -min -add_delay 0.330 [get_ports {sDAC_Data_0[*]}]
set_output_delay -clock [get_clocks sDAC_Clkin_0] -max -add_delay 0.150 [get_ports {sDAC_Data_0[*]}]
Once the constraints file is updated run synthesis, implementation, and generate a bitstream. Upon completion, verify there are no errors or critical warnings and export the hardware for use in Vitis. Under the File menu, select the 'Export Hardware...' option under 'Export'. Verify you are exporting to the same location as the existing hardware platform (XSA file) in the Vitis workspace and check the option to include the bitstream.
Launch Vitis from the Tools menu in Vivado and select the existing workspace for the project. Again, I'm just modifying the existing bare-metal application from my last project tutorial here, so refer to that for creating a new Vitis project and application.
The way the ZMOD bare-metal libraries function for both the ADC and the DAC is that they fill a buffer from DDR memory on the Eclypse via a DMA exchange. For the DAC, the original design from Digilent only has the read channel (memory map to stream or MM2S) enabled. Since we enabled the write channel (stream to memory map or S2MM) the libraries need to also be update to reflect that there is now a DMA instance that is bidirectional.
Starting with the DMA files in <Vitis workspace directory>/<bare metal application directory>/zmodlib/Zmod/ I added a new type to the dma_direction enumerator in dma.h to specify a DMA instance capable of both read and write transactions. The function for one way DMA transfers didn't allow for a direction change after the ZMOD instance was already created so I added two functions for the bidirectional type DMA, one for S2MM transfers and one for MM2S transfers.
Modified dma.h:
#ifndef DMA_H_
#define DMA_H_
#include <stdint.h>
/**
* Direction of a DMA transfer.
*/
enum dma_direction {
DMA_DIRECTION_TX, ///< TX transfer
DMA_DIRECTION_RX, ///< RX transfer
DMA_DIRECTION_TRX ///< TX & RX transfer
};
uint32_t fnInitDMA(uintptr_t addr, enum dma_direction direction, int dmaInterrupt);
void fnDestroyDMA(uintptr_t addr);
int fnOneWayDMATransfer(uintptr_t addr, uint32_t *buf, size_t length);
int fnS2MM_DMATransferCont(uintptr_t addr, uint32_t *buf, size_t transfer_size, int num_transfers);
int fnMM2S_DMATransfer(uintptr_t addr, uint32_t *buf, size_t transfer_size);
uint8_t fnIsDMATransferComplete(uintptr_t addr);
void* fnAllocBuffer(uintptr_t addr, size_t size);
void fnFreeBuffer(uintptr_t addr, void *buf, size_t size);
#endif /* DMA_H_ */
New DMA transfer functions added to dma.c:
S2MM DMA transfer function:
int fnS2MM_DMATransferCont(uintptr_t addr, uint32_t *buf, size_t transfer_size, int num_transfers){
DMAEnv *dmaEnv = (DMAEnv *)addr;
if (!dmaEnv)
return -1;
dmaEnv->complete_flag = 0;
if(dmaEnv->direction != DMA_DIRECTION_TRX){
return -1;
} else {
// S2MM - read in DDS data
// Associate data buffer
writeDMAReg(dmaEnv->base_addr, AXIDMA_REG_ADDR_S2MM_DA, (uint32_t)buf);
// Set DMA RX Run bit, value 1, DMA register
writeDMARegFld(dmaEnv->base_addr, AXIDMA_REGFLD_S2MM_DMACR_RUNSTOP, 1);
for (int i=0;i<num_transfers;i++){
// Start DMA Transfer
writeDMAReg(dmaEnv->base_addr, AXIDMA_REG_ADDR_S2MM_DA_LENGTH, transfer_size);
}
}
return 0;
}
MM2S DMA transfer function:
int fnMM2S_DMATransfer(uintptr_t addr, uint32_t *buf, size_t transfer_size){
DMAEnv *dmaEnv = (DMAEnv *)addr;
if (!dmaEnv)
return -1;
dmaEnv->complete_flag = 0;
if(dmaEnv->direction != DMA_DIRECTION_TRX){
return -1;
} else {
// MM2S - write out to DAC
// Associate data buffer
writeDMAReg(dmaEnv->base_addr, AXIDMA_REG_ADDR_MM2S_SA, (uint32_t)buf);
// Set DMA RX Run bit, value 1, DMA register
writeDMARegFld(dmaEnv->base_addr, AXIDMA_REGFLD_MM2S_DMACR_RUNSTOP, 1);
// Start DMA Transfer
writeDMAReg(dmaEnv->base_addr, AXIDMA_REG_ADDR_MM2S_SA_LENGTH, transfer_size);
}
return 0;
}
The DMA functions are called from the ZMOD base library, so it needed two functions added to it as well. One to kick off a MM2S DMA transaction, and one to kick off a S2MM DMA transaction (don't forget to also add these function prototypes to Zmod.h).
New Zmod.cpp functions:
/**
* Start a DMA S2MM transfer using the transfer length configured previously.
*
* @return 0 on success, any other number on failure
*/
int ZMOD::startS2MMTransferCont(uint32_t* buffer, int num_transfers){
// transfer length is not configured
if (transferSize < 1) {
return ERR_FAIL;
}
return fnS2MM_DMATransferCont(dmaAddr, buffer, transferSize, num_transfers);
}
/**
* Start a DMA MM2S transfer using the transfer length configured previously.
*
* @return 0 on success, any other number on failure
*/
int ZMOD::startMM2STransfer(uint32_t* buffer){
// transfer length is not configured
if (transferSize < 1) {
return ERR_FAIL;
}
return fnMM2S_DMATransfer(dmaAddr, buffer, transferSize);
}
Finally, in the DAC ZMOD specific library, two new functions were needed to write a period of sine wave to DDR from the DDS Compiler via a DMA S2MM transaction and read that data from the DDR into a buffer to send to the DAC via a DMA MM2S transaction. The initialization of the DAC instance also needed to be modified to indicate that the DMA attached to it is now bidirectional (capable of bother read/MM2S and write/S2MM transactions).
The modified the init instance function for the DAC ZMOD to use the new bidirectional type for the DMA:
ZMODDAC1411::ZMODDAC1411(uintptr_t baseAddress, uintptr_t dmaAddress, uintptr_t iicAddress, uintptr_t flashAddress, int dmaInterrupt)
: ZMOD(baseAddress, dmaAddress, iicAddress, flashAddress, DMA_DIRECTION_TRX, -1, dmaInterrupt)
{
ZMOD::initCalib(sizeof(CALIBECLYPSEDAC), ZMODDAC1411_CALIB_ID, ZMODDAC1411_CALIB_USER_ADDR, ZMODDAC1411_CALIB_FACT_ADDR);
}
Function to write DDS compiler output to DDR memory:
/*
* Reads in the data values being output by the DDS Compiler and writes them to a
* memory location in the DDR
* @param none
* @return the status: ERR_SUCCESS for success*/
int ZMODDAC1411::readInDDSdata(uint32_t* buffer, size_t &length, int num_transfers){
uint8_t Status;
if(length > ZmodDAC1411_MAX_BUFFER_LEN){
length = ZmodDAC1411_MAX_BUFFER_LEN;
}
// DMA TX transfer length in number of elements
// multiply by the size of the data
setTransferSize(length * sizeof(uint32_t));
// Start DMA Transfer
Status = startS2MMTransferCont(buffer, num_transfers);
if (Status) {
return ERR_FAIL;
}
return ERR_SUCCESS;
}
Function to read the DDS output data from DDR into the buffer for the DAC ZMOD:
int ZMODDAC1411::sendDDSdataToDAC(uint32_t* buffer, size_t &length){
uint8_t Status;
if(length > ZmodDAC1411_MAX_BUFFER_LEN)
{
length = ZmodDAC1411_MAX_BUFFER_LEN;
}
// DMA TX transfer length in number of elements
// multiply by the size of the data
setTransferSize(length * sizeof(uint32_t));
// Start DMA Transfer
Status = startMM2STransfer(buffer);
if (Status) {
return ERR_FAIL;
}
// // Wait for DMA to Complete transfer
// while(!isDMATransferComplete()) {}
return ERR_SUCCESS;
}
With the ZMOD bare-metal libraries updated, the main function is fairly straightforward. The main function first creates an instance of the DAC ZMOD, then sets the 14 bits output sample frequency divider and the gain value for channel one. Memory is then allocated for the buffer to read data into DDR from the DDS at the maximum length the DMA is capable of (0x3fff or 16384), and another buffer allocated to send data to the DAC.
One period of a 1MHz sine wave is being read from DDR memory (which equates to 50 samples) into the first buffer. It is then copied into the second buffer redundantly until the buffer is full. That buffer is then sent to the DAC and the DAC is started. Both buffers' memories are freed once the data has been sent to the DAC successfully, and the DAC runs infinitely outputting the 1MHz sine wave.
For the ADC, I just reused the ADC Demo functions from Digilent that I used in the last project. This ADC demo function starts the ADC to continuously capture in and infinite loop and format the data to output to the UART console.
Main function code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "xaxidma.h"
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "./zmodlib/Zmod/zmod.h"
#include "./zmodlib/ZmodADC1410/zmodadc1410.h"
#include "./zmodlib/ZmodDAC1411/zmoddac1411.h"
#include "./zmodlib/Zmod/dma.h"
#define TRANSFER_LEN 0x400
// ZMOD ADC parameters
#define ZMOD_ADC_BASE_ADDR XPAR_AXI_ZMODADC1410_0_S00_AXI_BASEADDR
#define DMA_ADC_BASE_ADDR XPAR_AXI_DMA_ADC_BASEADDR
#define IIC_BASE_ADDR XPAR_PS7_I2C_1_BASEADDR
#define FLASH_ADDR_ADC 0x30
#define ZMOD_ADC_IRQ XPAR_FABRIC_AXI_ZMODADC1410_0_LIRQOUT_INTR
#define DMA_ADC_IRQ XPAR_FABRIC_AXI_DMA_ADC_S2MM_INTROUT_INTR
//ZMOD DAC parameters
#define ZMOD_DAC_BASE_ADDR XPAR_AXI_ZMODDAC1411_V1_0_0_BASEADDR
#define DMA_DAC_BASE_ADDR XPAR_AXI_DMA_DAC_BASEADDR
#define FLASH_ADDR_DAC 0x31
#define DMA_DAC_IRQ XPAR_FABRIC_AXI_DMA_DAC_MM2S_INTROUT_INTR
#define IIC_BASE_ADDR XPAR_PS7_I2C_1_BASEADDR
//DMA for DDS output - XPAR_AXI_DMA_DDS_DEVICE_ID
#define MEM_BASE_ADDR (XPAR_PS7_DDR_0_S_AXI_BASEADDR + 0x1000000)
#define TRX_BUFFER_BASE (MEM_BASE_ADDR + 0x00100000)
#define TRX_BUFFER_HIGH (MEM_BASE_ADDR + 0x004FFFFF)
/*
* Simple ADC test, puts the ADC in the test mode (ramp),
* performs an acquisition under specific trigger conditions
* and verifies the acquired data to be consistent with these conditions.
*/
void testZMODADC1410Ramp_Auto(){
ZMODADC1410 adcZmod(ZMOD_ADC_BASE_ADDR, DMA_ADC_BASE_ADDR, IIC_BASE_ADDR, FLASH_ADDR_ADC,
ZMOD_ADC_IRQ, DMA_ADC_IRQ);
if(adcZmod.autoTestRamp(1, 0, 0, 4, TRANSFER_LEN) == ERR_SUCCESS){
xil_printf("Success autotest ADC ramp\r\n");
} else {
xil_printf("Error autotest ADC ramp\r\n");
}
}
/*
* Format data contained in the buffer and sends it over UART.
* It displays the acquired value (in mV), raw value (as 14 bits hexadecimal value)
* and time stamp within the buffer (in time units).
* @param padcZmod - pointer to the ZMODADC1410 object
* @param acqBuffer - the buffer containing acquired data
* @param channel - the channel where samples were acquired
* @param gain - the gain for the channel
* @param length - the buffer length to be used
*/
void formatADCDataOverUART(ZMODADC1410 *padcZmod, uint32_t *acqBuffer, uint8_t channel, uint8_t gain, size_t length){
char val_formatted[15];
char time_formatted[15];
uint32_t valBuf;
int16_t valCh;
float val;
xil_printf("New acquisition ------------------------\r\n");
xil_printf("Ch1\tRaw\tTime\t\r\n");
for (size_t i = 0; i < length; i++){
valBuf = acqBuffer[i];
valCh = padcZmod->signedChannelData(channel, valBuf);
val = padcZmod->getVoltFromSignedRaw(valCh, gain);
padcZmod->formatValue(val_formatted, 1000.0*val, "mV");
if (i < 100){
padcZmod->formatValue(time_formatted, i*10, "ns");
} else {
padcZmod->formatValue(time_formatted, (float)(i)/100.0, "us");
}
xil_printf("%s\t%X\t%s\r\n", val_formatted, (uint32_t)(valCh&0x3FFF), time_formatted);
}
}
/*
* Simple ADC test, acquires data and sends it over UART.
* @param channel - the channel where samples will be acquired
* @param gain - the gain for the channel
* @param length - the buffer length to be used
*/
void adcDemo(uint8_t channel, uint8_t gain, size_t length){
ZMODADC1410 adcZmod(ZMOD_ADC_BASE_ADDR, DMA_ADC_BASE_ADDR, IIC_BASE_ADDR, FLASH_ADDR_ADC,
ZMOD_ADC_IRQ, DMA_ADC_IRQ);
uint32_t *acqBuffer;
adcZmod.setGain(channel, gain);
while(1){
acqBuffer = adcZmod.allocChannelsBuffer(length);
adcZmod.acquireImmediatePolling(acqBuffer, length);
formatADCDataOverUART(&adcZmod, acqBuffer, channel, gain, length);
adcZmod.freeChannelsBuffer(acqBuffer, length);
sleep(2);
}
}
int main(){
init_platform();
xil_printf("Eclypse Z7 SDR baseband data generator...\r\n");
// init DAC Zmod
ZMODDAC1411 dacZmod(ZMOD_DAC_BASE_ADDR, DMA_DAC_BASE_ADDR, IIC_BASE_ADDR, FLASH_ADDR_DAC, DMA_DAC_IRQ);
// max buffer length:
size_t length = 0x3fff;
dacZmod.setOutputSampleFrequencyDivider(2);
dacZmod.setGain(0, 1);
int Status = 0;
uint32_t *TrxBufferPtr;
TrxBufferPtr = dacZmod.allocChannelsBuffer(length);
for (int i=0;i<16383;i++){
TrxBufferPtr[i] = 0;
}
uint32_t *acqBufferPtr;
acqBufferPtr = dacZmod.allocChannelsBuffer(length);
Status = dacZmod.readInDDSdata(TrxBufferPtr, length, 1);
if (Status) {
xil_printf("DMA MM2S error!...\r\n");
}
int start_index = 0;
// copy the one period of sine wave into the buffer until its full
while (start_index<16000){
for (int i=0;i<50;i++){
acqBufferPtr[start_index+i] = TrxBufferPtr[i];
}
start_index = start_index + 50;
}
Status = dacZmod.sendDDSdataToDAC(acqBufferPtr, length);
if (Status) {
xil_printf("DMA S2MM error!...\r\n");
}
// start the instrument
dacZmod.start();
// free the buffers since it's been transferred to the DAC
dacZmod.freeChannelsBuffer(TrxBufferPtr, length);
dacZmod.freeChannelsBuffer(acqBufferPtr, length);
// start channel 0 of the ADC collecting infinitely
adcDemo(0, 0, length);
cleanup_platform();
return 0;
}
Save all files and build the project.
To run the application and see the ILAs at the same time, first program the Eclypse by right-clicking on the application name in the Explorer window and selecting 'Program FPGA'.
Be sure to change the bitstream to the new bitstream. When you click the 'Search...' button next to the bitstream field, you'll see that Vitis detects both in the project.
After programming the Ecylpse, launch a debug run of the applicaton by again right-clicking on the application name in the Explorer window and selecting 'Launch on Hardware (Single Application Debug)' under 'Debug As'.
Once the application hits the main function entry breakpoint, connect to the UART of the Eclypse using the Vitis serial terminal.
Before stepping through the application or running it, switch back to Vivado and open the Hardware Manager from the Flow Navigator. Select auto-connect from the Open Target option.
You may need to reprogram the FPGA from the Hardware Manager if the ILA windows open and there are no debug cores present. I've found this to be a bug with Vivado version 2019.2.
Arm the trigger in the ILA on whichever of the AXI stream protocols of interest and run the application in Vitis. I monitored the master AXI stream port of the DDS Compiler and the AXI stream S2MM port of the DAC's DMA for debugging on this project.
Once I was able to see the sine wave loopback working in the ILAs and the printout in the UART console from the ADC ZMOD, I decided to fire up my vintage spectrum analyzer living on the top shelf of my book case to see what the 1MHz sine wave actually looked like coming out of the SMA port of the DAC port.
My spectrum analyzer was actually meant for TV repair so the RF input is 75-ohm, so using a 75-to-50-ohm balun and some adapter cables to convert from the BNC input of it to the SMA port of the ZMOD and get the Eclypse connected.
As you can see, for a pure continuous sine wave that's supposed to be a single frequency, it is quite messy with a range of extra frequency components. I'm sure this has something to do with my absurdly long SMA cable and hacked together connection conversion. This spectrum analyzer also hasn't been properly calibrated since 1984, but it's still great to use to just see that there is indeed an analog signal coming out of the DAC ZMOD at 1MHz.
I found that the SMA cables I own are much longer than any of the USB cable I own, so I ended up leaving the signal analyzer on the top of my bookshelf and the Eclypse board down on my desk nest to my computer.
Why do I own longer SMA cables than USB cables??? Good question. I just discovered this anomaly myself.
Overall, the design can be modified in a pretty straightforward manner to make the phase increment and offset input of the DDS compiler programmable and ultimately make a baseband data generator out of the Eclypse with ZMODs. I'll save that for another project tutorial!
Comments