Skip to end of metadata
Go to start of metadata

Introduction

This tutorial shows how to use the µC/OS BSP to create a basic application on the Zynq®-7000 using the Vivado IDE and Xilinx® SDK. In this tutorial, you will use the Vivado IP Integrator to configure a Zynq processor system as well as integrating soft peripherals in the FPGA fabric. You will then use the µC/OS BSP to generate a basic application using the µC/OS-III real time kernel.

A video version of this tutorial is also available on the Micrium YouTube channel :

The tutorial will allow you to experiment with the following concepts :

  • Generation of a µC/OS-III application and BSP
  • BSP and driver configuration
  • Select and use Xilinx standalone drivers and Micrium custom drivers
  • Standard output (Text traces)
  • Interrupt handling

This tutorial is not meant as a demonstration of the Vivado design flow. Readers are recommended to first go through Xilinx official tutorials and documentation before attempting this tutorial.

Software Requirements

  1. A suitable Vivado Design Suite is required for this tutorial. The WebPACK edition can be used if your board is supported.
  2. The µC/OS BSP. The BSP is distributed with the full source code of µC/OS-III for evaluation purpose. See installation instructions for setup.

Vivado Design Suite version 2014.3 is used within this tutorial. Screenshot and design steps may differ in other versions.

Hardware Requirements

This tutorial was written and tested on the Xilinx ZC702, ZC706, Avnet ZedBoard™ and MicroZed. However, most if not all Zynq based development platforms should be suitable for this tutorial. For best results the board should have an available UART output connected to the PS UART.

Make sure you have the proper debugger or debugging cable connected and that the board jumpers are configured accordingly.

Hardware Design

Before writing a software application for any Xilinx programmable devices it is first required to create the hardware design. This tutorial aims at building a hardware design with the following components.

  • Cortex-A9 hardened processor
  • Standard output via the processor system's UART
  • Basic FPGA design
    • AXI Interconnect connected to the PS general purpose AXI4 master port
    • Two soft AXI Timer

    • Routing of the two timers to the PS

Step 1. Invoke the Vivado IDE and Create a project

1. Open the Vivado IDE as the start page. Figure - Vivado Start Page

 

Figure - Vivado Start Page

 

2. From the getting started page click "Create New Project". This should open the new project wizard. Figure - Vivado New Project Wizard Click Next.

 

Figure - Vivado New Project Wizard

 

3. Type in the project name and location. Make sure "Create project subdirectory" is checked. Click Next.

4. Select "RTL Project" as the project type and check the "Do not specify sources at this time" checkbox. Click Next.

5. In the default part dialog select you're board or part. Click Next.

6. In the project summary page click Finish.

This should bring you in the main Vivado IDE project view with a blank project. Figure - Vivado Project View

 

Figure - Vivado Project View

Step 2. Create an IP Integrator Design

1. In the flow navigator select the "Create Block Design" item. Figure - Block Design Flow Items

 

Figure - Block Design Flow Items

 

2. Specify a name for the block design and click OK.

Step 3. Add and setup the Zynq processor system IP block

1. Open the Add IP dialog by right clicking in the block diagram canvas and selecting "Add IP...". Figure - Add IP Context Menu

 

Figure - Add IP Context Menu

 

2. In the search field type "Zynq" and then select ZYNQ7 Processing System, finally press enter to add the Zynq IP block to the design. Figure - Add Zynq IP

 

Figure - Add Zynq IP

 

You should now see the Zynq block alone in the middle of the block design schematic. Figure - Zynq Block

 

Figure - Zynq Block

 

3. Invoke the block automation dialog for the Zynq. This should be highlighted in green at the top of the canvas and can be started by clicking "Run Block Automation". Figure - Zynq Block Automation Assistance

Block automation for the Zynq7 Processing System will only be available if Vivado is aware of the board you are using.

Figure - Zynq Block Automation Assistance

 

4. In the "Run Block Automation" dialog select "Apply Board Preset" and leave the cross trigger settings to disable. Click OK. Figure - Zynq Block Automation Dialog

 

Figure - Zynq Block Automation Dialog

 

If all went well block automation should have connected the external memory and fixed I/Os automatically. Figure - Zynq Block Automation Result

 

Figure - Zynq Block Automation Result

Step 4. Customize the Zynq block for our design

In the previous step the Zynq7 IP block was added to the design. In this step you will customize the Zynq block for the tutorial design and connect the two AXI Timers that will be used for the software demonstration.

For the software demonstration many components are needed.

UART - To output string message the UART must be wired to an external source.

Reference clock - The Zynq can export up to four clock signals to the FPGA fabric. One is needed to clock the AXI timers and interconnect.

General Purpose Master Port - To access a soft peripheral located in the FPGA one of the AXI4 master port of the PS must be activated.

Interrupt - To route back the interrupts of the timers to the PS the PS-PL interrupts must be enabled.

 

1. Open the Zynq7 customize dialog by right clicking on the block and selecting "Customize Block". Figure - Zynq Block Context Menu

 

Figure - Zynq Block Context Menu

 

The re-customize IP dialog for the Zynq7 should now open. Figure - Zynq Block Context Menu

 

Figure - Zynq Block Context Menu

 

2. Make sure the AXI General Purpose Master Port 0 is enable in the PS-PL Configuration section. This is usually the done by default when invoking the Zynq block. Figure - Zynq PS-PL Configuration

 

Figure - Zynq PS-PL Configuration

 

3. Setup a UART output. Most board will have this configured by default in the "Peripheral I/O Pins" section. Figure - Zynq Peripheral IO Configuration

 

Figure - Zynq Peripheral IO Configuration

 

4.Setup a 50Mhz clock from the PS to the PL. This is usually the default setup in the "Clock Configuration" section. Figure - Zynq Clock Configuration

 

 
Figure - Zynq Clock Configuration

 

5.Enable the FPGA to PS interrupt lines in the "Interrupts" section. This will enable routing up to 16 individual interrupts from the FPGA to the Cortex-A9 interrupt controller. Figure - Zynq Interrupts Configuration

 

 
Figure - Zynq Interrupts Configuration

 

6. Cick OK in the "Re-customize IP" dialog.

Step 5. Add the soft peripherals

Now that the Zynq block is properly configured it's time to add the soft timers. these timers will then be connected to the Zynq master port via an AXI interconnect and mapped into the main ARM interconnect address space.

 

1. Add two AXI Timer to the block design. This can be done in a similar fashion as the Zynq block by right clicking in the canvas and then select Add IP. From the Add IP dialog search for "AXI Timer" and add it to the design. Repeat once for the second timer. Figure - Zynq and Unconnected Timers Schematic

 

 
Figure - Zynq and Unconnected Timers Schematic

 

2. Connection automation, available from the highlighted green bar, can be used to connect the timers automatically. Figure - Timers Connection Automation

 

 
Figure - Timers Connection Automation

 

The timers should now be connected to the Zynq block via a new AXI Interconnect automatically added by the connection automation design assistance. You may notice that a reset processing system is also part of the design now. Figure - Timers Connection Completed

 

 
Figure - Timers Connection Completed

 

Connection automation also automatically assigns address ranges to the peripheral being connected. You can consult the resulting address map in the "Address Editor" pane of the block diagram editor. Figure - Timers Address Configuration

 

 
Figure - Timers Address Configuration

 

3. Add a single "Concat" block instance from the "Add IP" dialog. This block can be used to aggregate the interrupt signals routed to the PS. The output of this block should be connected to the IRQ_F2P[0:0] port of the Zynq block. Then the interrupt line of axi_timer_0 connected to pin 0 of the Concat block. The same should be done to axi_timer_1 and connected to pin 1 of the Concat block. The final schematic should look like Figure - Final Hardware Design Schematic

 

 
Figure - Final Hardware Design Schematic

 

4. The design can be checked for errors by clicking the "Validate Design" button.

 

Step 6. Generate HDL Design Files

1. In the Sources tree right click the block design file and select "Generate Output Files". Figure - Generate Output Files Context

 

 
Figure - Generate Output Files Context

 

2. Right click again and select "Create HDL Wrapper" this time. When prompted select "Let Vivado Manage Created Wrappers".

 

Step 7. Synthesis, Implement and Generate Bitstream

1. To perform the synthesis, implementation and bitstream generation all at once click "Generate Bitstream" in the "Program and Debug" section of the Flow Navigator. When prompted to run the synthesis and implementation step prior to generation click yes. Figure - Generate Bitstream

 

 
Figure - Generate Bitstream

 

After it's done you're presented with a dialog where you can choose to open the implemented design. From there you'll be able to review resource usage, timing information and the floorplan of the final implementation. The design is now ready to export to the Xilinx SDK.

2. To export the design select the "Export Hardware" option in the File menu. Make sure to select "Include Bitstream". Figure - Export Hardware Dialog

 

 
Figure - Export Hardware Dialog

 

3. Launch the SDK by selecting the "Lauch SDK" option, again in the File menu.

This concludes the hardware design portion of this tutorial.

Software Design

The goal of the software portion of this tutorial is to create a basic µC/OS-III project. Then demonstrate interfacing with the AXI Timer using both the Micrium custom driver and Xilinx standalone driver.

Step 1. Installation of the µC/OS Repository

Full installation instructions are available in the user manual.

1. To install the repository add it to the current workspace from the Xilinx Tools->Repositories menu. Figure - Xilinx SDK Repository Preferences

 

Figure - Xilinx SDK Repository Preferences

Step 2. Generate the µC/OS BSP

The first step is to generate the µC/OS BSP for the hardware platform and a simple µC/OS "Hello World" type project.

 

1. Open the Xilinx SDK. This should have been done as the last step of the Hardware Design section. Figure - Xilinx SDK Main Screen

 

Figure - Xilinx SDK Main Screen

 

2. Open the "New Application Project Dialog". It can be reached from the File->New->Application Project menu.

In this dialog, enter the project name and select "ucos" as the OS Platform. Click Next. Figure - New Application Project Dialog

 

 
Figure - New Application Project Dialog

 

3. The New Project Template dialog should appear next. Select uC/OS-III Hello World then click Finish. Figure - New Project Template Dialog

 

 
Figure - New Project Template Dialog

 

You should now see the Board Support Package summary in the IDE.

 

4. Open the Board Support Package Settings dialog by clicking "Modify this BSP's Settings".

5. Select the necessary libraries ucos_common, ucos_osiii and ucos_standalone. Figure - BSP Settings Overview

The ucos_common library is always required by the BSP as well as one of the kernel either ucos_osii or ucos_osiii but not both. The ucos_standalone package is a compatibility component enabling the use of Xilinx standalone drivers. Details on the library components can be found in the Supported Micrium Products section of the user manual with links to the documentation.

 

 
Figure - BSP Settings Overview

 

6. Select the STDOUT provider in the "ucos" configuration section.For the ZC702 and ZedBoard this should be ps7_uart_1. Figure - BSP STDOUT Setting

 

 
Figure - BSP STDOUT Setting

 

7. Configure the drivers for the AXI Timers. In this tutorial axi_timer_0 will be programmed with the µC/OS custom driver while axi_timer_1 will use the Xilinx standalone driver. Figure - BSP Drivers Configuration

 

Figure - BSP Drivers Configuration

 

8. Click OK.

Step 3. Build and Debug the Demonstration Project

The default project generated is a simple Hello World message printed from the main task.

1. Build the project. This is usually done automatically by the Xilinx SDK after modifying the BSP configuration.

2. Select the project (Not the BSP) in the workspace and open the debug configuration dialog from the Run->Debug Configurations... menu.

3. Create a new debug configuration by double clicking "Xilinx C/C++ Application (System Debugger)".

4. Check the "Reset Entire System" and "Program FPGA" in the newly created debug configuration. This will automatically program the FPGA when starting a debug session. Figure - Debug Configuration

 

Figure - Debug Configuration

 

5. Click Debug.

After programming the FPGA the debugger should now be stopped at the main () function.

6. Connect a terminal to the COM port of you development board. The embedded terminal or any other terminal app can be used. Figure - Terminal Configuration

 

Figure - Terminal Configuration

 

7. Run the demonstration by pressing Run->Resume or F8. You should see text output in the terminal. Figure - Terminal Output

 

 
Figure - Terminal Output

 

Step 4. Program the AXI Timer 0 with the ucos_axitimer Driver

For some peripherals, Micrium distributes custom drivers which are usually designed to be thread safe for use by RTOS services. For example, the ucos_axitimer primary role is to act as a driver for the kernel timebase of MicroBlaze systems. These drivers are made available for general usage for convenience.

In this step you will create a new kernel task waiting on a semaphore periodically posted by an interrupt service routine. This ISR will be triggered by the AXI Timer 0 using the ucos_axitimer. In step 5 the same manipulation will be done using the Xilinx standalone driver.

For clarity the error codes returned by the various kernel functions are not checked in the following examples. Errors should usually be validated in a final application.

 

1. Create a new task and semaphore.

The first step is to declare the task function, it's TCB (Task Control Block) and stack space near the top of app.c. At the same time we need a single semaphore named Timer0Semaphore in this example. Listing - Timer 0 Task Declarations

 

void    Timer0Task (void *p_arg);
OS_TCB  Timer0TCB;
CPU_STK Timer0TaskStk[512];

OS_SEM  Timer0Semaphore;
Listing - Timer 0 Task Declarations

 

A bare task in µC/OS-III is a simple function, Timer0Task in this example. To help the demo we can add a UCOS_Print() at the start of the new task to ensure it was created successfully. See Listing - Timer 0 Task Skeleton Since we do not want this task to return a while(1) is added near the end of the function.

 

void Timer0Task (void *p_arg)
{
    OS_ERR os_err;

    UCOS_Print("Timer0Task reached\r\n");

	while (1) {
	}
}
Listing - Timer 0 Task Skeleton

The UCOS_Print() implementation is re-entrant (Thread safe) which means it can be called from multiple tasks without special synchronization.

 

A semaphore is used in this example to await the signal from the timer. Creating a semaphore in µC/OS-III is a simple function call as illustrated in Listing - Timer 0 Semaphore Create

 

OSSemCreate(&Timer0Semaphore, "Timer 0 Semaphore", 0, &os_err);
Listing - Timer 0 Semaphore Create

 

The Timer 0 task can pend (Wait) on this semaphore once it's created with the OSSemPend() and output something on the terminal when it is signaled. Finally the task should be created with the OSTaskCreate() function call. Listing - Timer 0 Task shows the current content of the main and Timer0 tasks.

 

void  MainTask (void *p_arg)
{
    OS_ERR       os_err;
    
    UCOS_Print("Hello world from the main task\r\n");

    OSSemCreate(&Timer0Semaphore, "Timer 0 Semaphore", 0, &os_err);

    OSTaskCreate(&Timer0TCB,
                  "Timer 0 Task",
                  Timer0Task,
                  DEF_NULL,
                  10,
                  Timer0TaskStk,
                  0,
                  512,
                  0,
                  0,
                  DEF_NULL,
                  0,
                 &os_err);

    while (DEF_TRUE) {
        OSTimeDlyHMSM(0, 0, 10, 0, OS_OPT_TIME_HMSM_STRICT, &os_err);
        UCOS_Print("Periodic output every 10 seconds from the main task\r\n");
    }
}

void Timer0Task (void *p_arg)
{
    OS_ERR os_err;

    UCOS_Print("Timer0Task reached\r\n");

    while (1) {
        OSSemPend(&Timer0Semaphore, 0, 0, DEF_NULL, &os_err);
        UCOS_Print("Timer 0 Semaphore signaled\r\n");
    }
}
Listing - Timer 0 Task

 

Running the program now will show the Timer 0 starting but pending indefinitely on the semaphore since the timer isn't configured yet.

2. Configure the AXI Timer 0 to signal the Timer 0 semaphore.

Micrium custom drivers usually register a default interrupt handler when initialized. The interrupt source is deducted from the hardware design. In the case of the ucos_axitimer driver it's possible to register a callback to be invoked when the interrupt trigger. The function used is shown in Listing - Timer 0 ISR

 

void Timer0ISR (AXITIMER_HANDLE handle, CPU_INT32U tmr_nbr)
{
    OS_ERR os_err;
    OSSemPost(&Timer0Semaphore, 0, &os_err);
}
Listing - Timer 0 ISR

 

The ISR simply post the Timer 0 semaphore.

The last step is to configure the AXI Timer. The public api of the driver can be accessed by including the ucos_axitimer.h header file to app.c. Micrium drivers are centered around handles which are returned by the various Init functions. Listing - Timer 0 Handle Declaration shows the declaration of an AXI Timer handle for Timer 0.

 

AXITIMER_HANDLE Timer0;
Listing - Timer 0 Handle Declaration

 

To configure the timer it must first be initialized, then configured as a countdown, auto-reload timer with interrupts enabled. In the hardware design the timer is driven by a 50MHz clock and we'll use a load value of 100 Million to give a 2 seconds delay between interrupts. Listing - Timer 0 Setup

 

    Timer0 = AXITimer_Init(0);
    AXITimer_OptSet(Timer0, 0, AXITIMER_OPT_DOWN | AXITIMER_OPT_AUTO_RELOAD | AXITIMER_OPT_INT);
    AXITimer_LoadSet(Timer0, 0, 100000000);
    AXITimer_CallbackSet(Timer0, 0, Timer0ISR);
Listing - Timer 0 Setup

 

Finally the timer can be started. Listing - Timer 0 Start

 

    AXITimer_Start(Timer0, 0);
Listing - Timer 0 Start

 

3. Run the application. The output should look like Figure - Timer 0 Terminal Output.

 

Figure - Timer 0 Terminal Output

Step 5. Program the AXI Timer 1 with the Xilinx tmrctr Driver

In step 4 the Micrium custom driver ucos_axitimer was used to generate a periodic interrupt waking an application task. The same can be accomplished by using the Xilinx standalone driver distributed with the SDK.

When using a standalone driver it's important to have the ucos_standalone library included in the project. Moreover, if the peripheral is used from multiple threads the necessary synchronization must be provided by the application, either by using kernel semaphores or mutexes.

1. Create a new task and semaphore similar to item 1 of step 4.

2. Write a custom interrupt service routine for the timer. Listing - Timer 1 ISR

 

void Timer1ISR (void *p_arg, CPU_INT32U cpu)
{
    CPU_INT32U ControlStatusReg;
    OS_ERR os_err;

    ControlStatusReg = XTmrCtr_ReadReg(Timer1.BaseAddress,
                                       0,
                                       XTC_TCSR_OFFSET);

    XTmrCtr_WriteReg(Timer1.BaseAddress,
                     0,
                     XTC_TCSR_OFFSET,
                     ControlStatusReg |
                     XTC_CSR_INT_OCCURED_MASK);

    OSSemPost(&Timer1Semaphore, 0, &os_err);
}
Listing - Timer 1 ISR

 

Raw interrupt routines under the µC/OS all have the same signature, part of which are only relevant in some contexts. The p_arg argument is a user specified parameter given when registering the interrupt. The cpu argument is the core id of the cpu that generated the interrupt and is only relevent for software generated interrupts on the cortex-A9. The cpu argument will be 0 in all other cases.

3. Register and enable the custom interrupt. Listing - Timer 1 Interrupt Configuration

 

    UCOS_IntVectSet(62, 0, DEF_BIT_00, Timer1ISR, &Timer1);

    UCOS_IntSrcEn(62);
Listing - Timer 1 Interrupt Configuration

 

4. Build and Run. The output should be similar to the previous output from step 4. The final app.c file can be downloaded here - app.c

Conclusion

In this tutorial you created a basic Zynq hardware design and wrote a basic application using the µC/OS BSP. The usage of both the Micrium custom drivers and Xilinx standalone drivers was presented along with interrupt handling. Readers new to the Micrium ecosystem are advised to read the uC-OS-III Documentation for in-depth information on using the Micrium real time kernel. On the other hand, long time Micrium users are strongly recommended to check the various tutorials and trainings of the Vivado Design Suite.

  • No labels