Microprocessors
Programs

A MicroZed UDP Server for Waveform Centroiding: 3.2

Table of Contents

3.2: The main.c and echo.c Files: Part 1

If you compare the main.c and echo.c files you just downloaded to the ones that come with the lwIP example, you'll see that we've added quite a bit of stuff. It may seem a bit overwhelming when you look through all the code at first, but don't fear. Like anything challenging in life, it just takes a bit of patience and perseverance.


There are four main components to the application, all of which are interrelated:

  1. The UDP server and the Receive Callback
  2. The GetCentroid IP Core
  3. The DMA Engine
  4. The Interrupt Controller and Interrupts

One approach would be to cover each of these as its own section. But because of how intertwined each of these components are, I'm going to take a more sequential-based approach and follow the code as it goes from initializing everything, to receiving UDP packets, to sending the index array and waveforms contained in those packets to the GetCentroid algorithm.


NOTE: when talking about particular project files, I'm going to go away from the arrow format (e.g. GetCentroid_bsp → ps7_cortexa9_0 → include) in favor of a directory structure format (e.g. GetCentroid_bsp/ps7_cortexa9_0/include). The Project Explorer just shows you the file structure of your project directory anyway. You can navigate the files through your filesystem just as easily.

3.2.1: Initializing the UDP Server, DMA, GetCentroid, and Interrupt Controller

In the very first lines of main() in main.c, you can see some obvious initialization calls like init_platform(). This function basically sets up some timers on the Zynq7010 and does a cursory startup of the interrupt controller. There is also initPeripherals(), which is where we can put the calls to initialize the hardware we'll be using.

3.2.1.1: The DMA and GetCentroid Init

initPeripherals() sets up our DMA and GetCentroid cores with the following set of calls:

 	               // Initialize GetCentroid core
   printf("Initializing GetCentroid\n\r");
   GetCentroid_cfg = XGetcentroid_LookupConfig(XPAR_GETCENTROID_0_DEVICE_ID);
   if (GetCentroid_cfg){
     int status = XGetcentroid_CfgInitialize(&GetCentroid, GetCentroid_cfg);
     if (status != XST_SUCCESS){
       printf("Error initializing GetCentroid core\n\r");
     }
   }

   // Initialize AxiDMA core
   printf("Initializing AxiDMA\n\r");
   axiDMA_cfg = XAxiDma_LookupConfig(XPAR_AXIDMA_0_DEVICE_ID);
   if (axiDMA_cfg){
     int status = XAxiDma_CfgInitialize(&axiDMA, axiDMA_cfg);
     if (status != XST_SUCCESS){
       printf("Error intializing AxiDMA core\n\r");
     }
   }

Both of these initializations rely on their base structures: XGetcentroid and XAxiDma, which we've declared earlier in the file:

 	               // IP Config Pointers and Handlers
   XGetcentroid GetCentroid;
   XGetcentroid_Config *GetCentroid_cfg;
   XAxiDma axiDMA;
   XAxiDma_Config *axiDMA_cfg;

You can take a look at xgetcentroid.h and xaxidma.h to see everything they contain. The important thing to note here is that these calls use the component's DEVICE_ID to look up its base memory map address so that we can use the high-level functions to see how its configured, or to program it through low-level register writes.

3.2.1.2: The Generic Interrupt Controller Init

The next thing we do is disable all the interrupts (it's very important to disable them before we start messing around with the registers!) and then make a call to the SetupIntrSystem(), which is a function we've defined ourselves as:

 	               static int SetupIntrSystem(INTC * IntcInstancePtr, XAxiDma * AxiDmaPtr, 
     u16 TxIntrId, XGetcentroid * GetCentroidPtr, u16 GetCentroidIntrId);

The function calls Xil_ExceptionInit(), Xil_ExceptionRegisterHandler(), Xil_ExceptionEnable(), XScuGic_LookupConfig(), and XScuGic_CfgInitialize() within SetupIntrSystem(), which all are pulled directly from Xilinx examples and don't need to be messed with, so I won't go into too many details on those. But in order to get our specific example working properly, we'll have to alter the priorities of the DMA/GetCentroid interrupts, which is something that isn't shown in many examples I've seen.


Notice in the lines:

                   /* Set priority for the TX DMA and GetCentroid Interrupts */
   XScuGic_SetPriorityTriggerType(IntcInstancePtr, GetCentroidIntrId, 0x90, 0x3);
   XScuGic_SetPriorityTriggerType(IntcInstancePtr, TxIntrId, 0xA0, 0x3);

we are setting the trigger to be a rising edge for both (the last argument: 0x3), but different interrupt priorities (0x90 vs 0xA0, with 0x00 being the highest and the 32 levels going in steps of 0x08) for the two peripherals.


Why are we setting different priorities? Well, we are going to be using these interrupts to signal a) when a given DMA transfer has completed and b) when a centroid value is ready at the output of GetCentroid. As I'll discuss later, we use global variables that are toggled in the interrupt handlers to signal that it's ok for the program to advance before doing another DMA transfer. If we give these equal priority, the program will eventually get stuck. This is based on my experience, and I'm not exactly sure why it happens. My best guess is that it has to do with the asynchronous nature of the Ethernet transfers and how nested interrupt are handled in the underlying Xilinx code. What I do know for sure is that we need to give GetCentroid the higher priority if we want to get the application to work as intended.


The next thing we do is connect the device driver handler that will be called when the interrupt occurs. We do this with a call to XScuGic_Connect(). Here, for instance, is how we do it for the GetCentroid interrupt:

                   Status = XScuGic_Connect(IntcInstancePtr, GetCentroidIntrId,
     (Xil_InterruptHandler)GetCentroidIntrHandler,
     GetCentroidPtr);

At a low level, what we are doing is giving it an address to jump to in the instruction set when there is an interrupt that has a matching Id (the Ids assigned to each component in your design can be found in xparameters.h). At a high level, we are writing a callback function, which is also known as interrupt service routine.

3.2.1.3: The Interrupt Callback Functions

In the previous code snippet, the third argument (Xil_InterruptHandler)GetCentroidIntrHandler is the name of a function that we define within main.c. Let's take a look at that function.


The only input to the function is void *Callback, which is a pointer to the instance of GetCentroid that generated the interrupt. We copy that pointer to XGetcentroidInst, and then get the value of the interrupt status register...

                   /* Read pending interrupts */
   IrqStatus = XGetcentroid_InterruptGetStatus(XGetcentroidInst);

followed by a check of the bits in that register like so:

                
                   // If it is an ap_done interrupt, we have values to read
   if (IrqStatus & 0x00000001){

     // Indicate that we've filled the arrays and are ready to send results
     SendResults = 1;

     // Clear the ap_done interrupt
     XGetcentroid_InterruptClear(XGetcentroidInst, 0x00000001);
   }

   // If it is an ap_ready interrupt, the algorithm is ready for new input data
   if (IrqStatus & 0x00000002){;

     // Indicate we are ready for more data in the algorithm
     GetCentroidReady = 1;

     // Clear the ap_ready interrupt
     XGetcentroid_InterruptClear(XGetcentroidInst, 0x00000002);

   }

You can see in the comments which bits in the interrupt status register correspond to which signals. How do we know this? All the bits are defined in the comment field of xgetcentroid_hw.h.

                
                  // 0x00c : IP Interrupt Status Register (Read/TOW)
  //    bit 0 - Channel 0 (ap_done)
  //    bit 1 - Channel 1 (ap_ready)

The two conditionals above do essentially the same thing. They set a global variable and then clear the interrupt. The first global variable tells our program we have a centroid value to send back over the Ethernet connection. The second global variable indicates that the algorithm is ready for a new waveform. You can take a look at the interrupt handler for the DMA controller, TxIntrHandler(), and see we do something very similar for that.

3.2.1.4: Enabling the Interrupts and GetCentroid

Once we return from SetupIntrSystem(), the last thing we do is we enable the interrupts we just configured and then enable the GetCentroid core. Note that we must call XGetcentroid_EnableAutoRestart() in order to enable autorestart, which basically means that once the algorithm has calculated a result, it can immediately start on a new waveform; it does not require the toggling of any control signals. If you don't call this function, you'll have to repeatedly call XGetcentroid_Start() each time you want to DMA data over. The lines that handle all of this are:

                   /* Enable all interrupts from AXI DMA */
   XAxiDma_IntrEnable(&axiDMA, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);

   // Enable interrupts from GetCentroid
   XGetcentroid_InterruptEnable(&GetCentroid, 0x00000003);
   XGetcentroid_InterruptGlobalEnable(&GetCentroid);

  // Setup and start the GetCentroid module (enable auto-restart to keep data flowing)
   XGetcentroid_EnableAutoRestart(&GetCentroid);
   XGetcentroid_Start(&GetCentroid);

And lastly, before we exit initPeripherals(), we initialize our three program-control variables: GetCentroidReady, DMA_TX_Busy, and SendResults so that things are ready to go.

3.2.2: Setting up the Ethernet Controller

Now that we've initialized the DMA engine, GetCentroid, and the interrupt handler, it's time to set up the Ethernet connection. If you've studied both versions of echo.c, you'll notice that there are some big differences between the two. Perhaps the most important is that the built-in version uses TCP, whereas we're using UDP. Notice we've removed the line that includes "lwip/tcp.h" and replaced it with one that includes "lwip/udp.h". It's beyond the scope of this tutorial to explain the nitty-gritty of these two protocols (plus, I'm not by any means a networking expert). But the main reason we're going with UDP is that it's a bit faster due to the fact that it has less overhead.


In fact, we're going to try to make Ethernet transactions go as quickly as possible by issuing several statements at the beginning of our code. Take a look at these #define statements:

                   //DEFINE STATEMENTS TO INCREASE SPEED
   #undef LWIP_TCP
   #undef LWIP_DHCP
   #undef CHECKSUM_CHECK_UDP
   #undef LWIP_CHECKSUM_ON_COPY
   #undef CHECKSUM_GEN_UDP

With the first one, we're going to remove the sections of the code that handle TCP/IP to make it have an even smaller footprint. With the second, we're going to prevent the program from taking 15-20 seconds at bootup to try and obtain an IP address via DHCP. DHCP isn't really necessary since we're already manually assigning it a MAC and IP address anyway. The last three statements are intended to remove overhead in the packet transactions by foregoing the checksums.

3.2.2.1: Network Addressing and Initialization

You can see where we are handling the network addressing with the following lines:

                   /* The mac address of the board. this should be unique per board */
   unsigned char mac_ethernet_address[] =
   { 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };

This assigns a fixed MAC address of 000a:3500:0102 to our Ethernet hardware, and

                   /* initialize IP addresses to be used */
   IP4_ADDR(&ipaddr, 192, 168, 1, 10);
   IP4_ADDR(&netmask, 255, 255, 255, 0);
   IP4_ADDR(&gw, 192, 168, 1, 1);

manually sets the IP address to 192.168.1.10, the gateway address to 192.168.1.1, and the subnet mask to 255.255.255.0. To talk to the our application, you just have to get on the same subnet with a 192.168.1.X IP address and you should be able to communicate just fine. More on that later.


The line

                   lwip_init();
            

calls the lwip_init() function defined in GetCentroid_bsp/ps7_cortexa9_0/ libsrc/lwip141_v1_7/src/lwip-1.4.1/src/core/init.c (or whatever version of lwip you have on your SDK version). This function is going to call a bunch of other initialization functions like mem_init() and udp_init() to get all the memory/buffer allocation, etc. set up properly.


The next several function calls deal with setting up the network interface by passing the our netif structure, which we simply call echo_netif, to several functions. Take a look at the netif structure defined in GetCentroid_bsp/ps7_cortexa9_0/ libsrc/lwip141_v1_7/src/lwip-1.4.1/src/include/lwip/netif.h. You'll see that this structure keeps track of a lot of important information and determines which functions to call when certain events occur on the network.

3.2.2.2: Setting up the UDP Listener

The next thing we do is make a call to start_application(), which is a function contained in echo.c. This function is extremely important. It creates a UDP Protocol Control Block (udp_pcb) and binds it to port 7. It then assigns the receive callback function with a call to udp_recv().

                   /* set the receive callback for this connection */
   udp_recv(pcb, recv_callback, NULL);

So all traffic directed to 192.168.1.10:7 (IP address 192.168.1.10 and port 7) will be handled with the function recv_callback(). The latter is an instance of the generic receive function

                   typedef void (*udp_recv_fn)(void *arg, struct udp_pcb *pcb, struct pbuf *p,
     ip_addr_t *addr, u16_t port);

which is defined in GetCentroid_bsp/ps7_cortexa9_0/ libsrc/lwip141_v1_7/src/lwip-1.4.1/src/include/lwip/udp.h. We'll describe what our callback function does in the next section.



← Previous   ...    Next →

Table of Contents