In my previous project post, I started a custom design with the Eclypse Z7 and the new Digitizer Zmod ADC breakout board for the AD9648. I got the hardware design laid out and exported from Vivado 2022.1, and now I'm following that up with the bare-metal/no-OS C application in Vitis 2022.1 to prove-in the pipeline of getting the ADC samples through to the ARM-core of the Zynq FPGA for any future desired processing.
Just to recap the overview of the data flow, I'm using Digilent's Zmod Digitizer Controller IP to clock in ADC data samples then writing them to DDR with an AXI DMA (direct memory access) device, using an AXI Stream FIFO with independent input and output clocks to handle the clock domain crossing from the ADC's DCO clock to the DMA/DDR's clock. The C application I'm writing in this post handles the set up of the DMA device and the kick off of it pulling in ADC data values and writing them to DDR, then writing them to the UART serial terminal. This creates the perfect generalized project/code template for future use in any project where I need an ADC like this on the Eclypse Z7 + Zmod platform.
Note: This project assumes a starting point of where my previous post left off.
New Vitis WorkspaceLaunch Vitis from the Vivado Tools Menu or directly from the command line. Upon launch, Vitis will ask for the location of the target workspace directory. To create a new workspace, simply point the Vitis launch window to an empty directory.
Personally, to keep everything organized and easy for my future self, I prefer to create folder in the top level directory of the corresponding Vivado project the hardware XSA file is exported from titled vitis_workspace
to create new Vitis projects in.
After launching into a new blank workspace, the first order of business is to create a platform project that takes the exported hardware XSA file and creates the hooks to the hardware for the software to run on, such as the BSP (board support package), boot components, etc.
Select the option to Create Platform Project. Point it to the exported XSA file from the corresponding Vivado project (see my previous post).
Since this application will be bare-metal/no-OS, the operating system is set to standalone running of the first of the two ARM-core processors of the Zynq FPGA chip.
When the new platform project is generated, it will initially show as out-of-date since no build files exist as of yet. Run a quick build on it with ctrl+B.
With the platform project complete, the next step is to create the application project. Select New > Application Project... Select the platform created by the platform project, then give the application project the desired name and verify the OS settings as standalone. Finally, select either the Hello World or Empty Application template. I went with the Hello World application project template just because there are a few things it preconfigured as compared to the Empty Application template.
Once the application project generates, the main application is located in ./src/helloworld.c
. It's up to you if you want to just edit it or create your own main.c
file.
I just added my DMA controller C code to helloworld.c
(see my helloworld.c
attached below). Starting with my function for kicking off a S2MM transfer of the DMA:
u32 XAxiDma_S2MMtransfer(XAxiDma *InstancePtr, UINTPTR BuffAddr, u32 Length){
u32 WordBits;
RingIndex = 0;
// Check scatter gather is not enabled
if (XAxiDma_HasSg(InstancePtr)){
xil_printf("Scatter gather is not supported\r\n");
return XST_FAILURE;
}
if ((Length < 1) || (Length > InstancePtr->RxBdRing[RingIndex].MaxTransferLen)){
xil_printf("Invalid transfer length.\r\n");
return XST_FAILURE;
}
if (!InstancePtr->HasS2Mm){
xil_printf("S2MM channel is not supported\r\n");
return XST_FAILURE;
}
// If the engine is doing transfer, cannot submit
if (!(XAxiDma_ReadReg(InstancePtr->RxBdRing[RingIndex].ChanBase, XAXIDMA_SR_OFFSET) & XAXIDMA_HALTED_MASK)){
if (XAxiDma_Busy(InstancePtr,XAXIDMA_DEVICE_TO_DMA)){
xil_printf("S2MM engine is busy\r\n");
return XST_FAILURE;
}
}
if (!InstancePtr->MicroDmaMode){
WordBits = (u32)((InstancePtr->RxBdRing[RingIndex].DataWidth) - 1);
} else {
WordBits = XAXIDMA_MICROMODE_MIN_BUF_ALIGN;
}
if ((BuffAddr & WordBits)){
if (!InstancePtr->RxBdRing[RingIndex].HasDRE){
xil_printf("Unaligned transfer without DRE %x\r\n", (unsigned int)BuffAddr);
return XST_FAILURE;
}
}
XAxiDma_WriteReg(InstancePtr->RxBdRing[RingIndex].ChanBase, XAXIDMA_DESTADDR_OFFSET, LOWER_32_BITS(BuffAddr));
if (InstancePtr->AddrWidth > 32){
XAxiDma_WriteReg(InstancePtr->RxBdRing[RingIndex].ChanBase, XAXIDMA_DESTADDR_MSB_OFFSET, UPPER_32_BITS(BuffAddr));
}
XAxiDma_WriteReg(InstancePtr->RxBdRing[RingIndex].ChanBase, XAXIDMA_CR_OFFSET, XAxiDma_ReadReg(InstancePtr->RxBdRing[RingIndex].ChanBase, XAXIDMA_CR_OFFSET)| XAXIDMA_CR_RUNSTOP_MASK);
// Writing length in bytes to the buffer transfer length register starts the transfer
XAxiDma_WriteReg(InstancePtr->RxBdRing[RingIndex].ChanBase, XAXIDMA_BUFFLEN_OFFSET, Length);
return XST_SUCCESS;
}
Overall the main function performs the following sequence of actions:
- Creates a DMA configuration pointer
- Initializes the memory locations the DMA will be writing the ADC values to to zeros
- Initializes/configures the AXI DMA device in the programmable logic (PL)
- Kicks off the stream to memory map (S2MM) transaction
- Prints the ADC values from DDR to the UART serial terminal
int main()
{
init_platform();
XAxiDma_Config *CfgPtr; //DMA configuration pointer
int Status, Index;
u8 *RxBufferPtr;
RxBufferPtr = (u8 *)RX_BUFFER_BASE;
// Initialize memory to all zeros
for(Index = 0; Index < MAX_PKT_LEN; Index ++){
RxBufferPtr[Index] = 0x00;
}
// Initialize the XAxiDma device
CfgPtr = XAxiDma_LookupConfig(DMA_DEV_ID);
if (!CfgPtr) {
xil_printf("No config found for %d\r\n", DMA_DEV_ID);
return XST_FAILURE;
}
Status = XAxiDma_CfgInitialize(&AxiDma, CfgPtr);
if (Status != XST_SUCCESS) {
xil_printf("Initialization failed %d\r\n", Status);
return XST_FAILURE;
}
if(XAxiDma_HasSg(&AxiDma)){
xil_printf("Device configured as SG mode \r\n");
return XST_FAILURE;
}
XAxiDma_IntrDisable(&AxiDma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA);
Xil_DCacheFlushRange((UINTPTR)RxBufferPtr, MAX_PKT_LEN);
XAxiDma_Reset(&AxiDma);
// Setup & kick off S2MM channel first
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;
}
for(Index = 0; Index < MAX_PKT_LEN; Index++) {
xil_printf("Received data packet %d: %x\r\n", Index, (unsigned int)RxBufferPtr[Index]);
}
XAxiDma_Reset(&AxiDma);
cleanup_platform();
return 0;
}
As I mentioned in my previous post, this project is intended to prove-in the pipeline of data from the ADC, to DDR memory via DMA (direct memory access), and then access those stored samples from a bare-metal C application. Which for now is just writing them to the UART serial terminal.
The max packet length variable is the number of bytes to be written to memory by the DDR. The tdata output from the Zmod Digitizer Controller IP is 32-bits (4 bytes) wide, so max packet length needs to be at least 4 in order to read one complete sample. This number also needs to account for the depth of the AXI Stream FIFO.
Once all of the edits have been made to the source code, save the files, and build the project (ctrl+B).
Launch Debug & Open ILAs in Vivado HW ManagerTo test the design I went back and added ILAs (integrated logic analyzers) on the input and output of the Zmod Digitizer Controller IP to view the ADC samples, and the output of the AXI Stream FIFO going into the S2MM port of the AXI DMA, then launched a debug run from Vitis on my Eclypse board.
Right-click on the application project's name in the Explorer window in Vitis and select Debug As > Launch Hardware (Single Application Debug). Once Vitis switches over to the Debug View and hits the breakpoint right inside the main function, open the serial terminal in Vitis.
Then open Vivado and launch the Hardware Manager from the Flow Navigator window. Use the Auto Connect option to connect to the ILAs in the Zynq FPGA.
Return back to Vitis and run the application to let it kick off the S2MM transfer in the DMA and print the read values to the serial terminal.
I set the triggers on the ILAs to start collecting when the tready control signals went high. Zooming in, I was able to see the ADC sample values come into the Zmod Digitizer Controller IP then pass through to the FIFO and ultimately be written to the DMA device. The print out of the values in the Vitis serial terminal is the final part of the data pipeline, verifying the system.
At this point, the Eclypse with Digitizer Zmod provides a quick and easy ADC setup for projects. I will be doing the same for my Scope and AWG Zmods then implementing them in my upcoming projects!
Comments