Learn how to create a dual-core ARM application using Xilinx's Vivado and Vitis tools to control programmable logic (PL) with both A53 and R5 processors.
The project features:
- A53 core running FreeRTOS controlling an blinking LED and monitoring a pushbutton
- R5 core running in standalone mode controlling a separate blinking LED and pushbutton
- AXI GPIO interfaces for LED control and push button monitoring
- Learn proper memory allocation between cores
- Adding new domain and second application project to your Vitis project
- Complete step-by-step implementation from block design to deployment
- Understand multi-core ARM development on Zynq UltraScale+
- Get hands-on experience with the ZCU104 evaluation board
You can find the full video of project here:
Block Diagram of designThe diagram depicts a dual-core ARM application featuring Cortex-A53 and Cortex-R5 cores. Here's a detailed breakdown:
- Cores and Their Roles:
- Cortex-A53 Runs a Free RTOS application that controls the DS40 LED. It reads the state of SW18 pushbutton through an AXI GPIO interface in polling mode. When the push button is hold, the A53 core starts blinking the DS40 LED by sending pulse signals over the AXI GPIO interface. It also prints “Hello world from A53” on serial terminal.
- The second core is Cortex-R5, it Operates as a standalone processor, and controls the DS39 LED similarly by monitoring SW17 in polling mode through another AXI GPIO interface. When SW17 is pressed, the R5 core starts blinking the DS39 LED, it prints “Hello world from R5” on serial terminal as long as the pushbutton is hold.
AXI Interconnect:
- Connects the AXI GPIO peripherals to the HPM0 (High-Performance Master Port 0), which is shared by both ARM cores. This setup allows the cores to send and receive AXI transactions to/from the GPIO interfaces.
DDR Memory:
- The DDR memory is partitioned into two regions, one for each core. This avoids memory contention and ensures that both cores operate independently without interfering with each other.
AXI GPIO for DS40 LED Control
Add an AXI GPIO to the block design to control DS40 LED. Set the direction to All Output and configure the bit width to 1 bit. Rename it to ARM0_SW_POLL, this name is only for better recognition and you can choose any desired names.
Expose the AXI GPIO output as an external port, rename the created port to ARM0_LED_PIN. During I/O planning, these ports will be assigned to the correct pins.
AXI GPIO for SW18 DIP Switch
Next, add another AXI GPIO for reading the SW18 pushbutton. Set its direction to Input and configure its bit width to 1 bit. Rename it to ARM0 switch for easier understanding.
Now we need to repeat the job to add two other AXI GPIO and connect them to next push button and LED. Add another AXI GPIO and make it as one bit output, this time use ARM1 during the renaming process. Another AXI GPIO will be as one bit input to read the SW17 in polled mode. These two AXI GPIO will be used with R5 to blinking the LED while the push button is hold.
During I/O planning, these ports will be assigned to the correct pins.
Auto connectionAt this point, we can let Vivado handle the connections automatically. Run the Auto Connection Automation feature, which will streamline the process. This will automatically add a Processor System Reset and an AXI Interconnect to the design.
Validate the design, Once the design is validated, Vivado will automatically assign addresses to the AXI GPIOs. Let’s have a look at the address editor, later on we can use these addresses in Vitis to access the GPIOs by MMIO.
Add the top wrapper and you can procced to synthesis the design.
Assigning the I/O portsat this stage, refer to the datasheet or user manual for your board to assign the correct input and output pins. If you are working with an evaluation board you can also use the board files to find those pins.
Assign the LED Pin and push button for first ARM core’s peripherals. Based on your evaluation board, assign the appropriate pins for the LED outputs and push button input.
Repeat the job for dedicated LED and push button for ARM core1.
Once the pins are assigned, save your I/O connections as an XDC file. If you're working with the ZCU104, I've also shared a pre-configured XDC file in the GitHub repository for convenience.
With everything set, you’re now ready to generate the bitstream. From the left-hand toolbar, select Generate Bitstream and wait for the process to complete.
Export XSAExporting the Hardware: The last step in Vivado is to export our hardware design for use in Vitis. Go to "File" > "Export" > "Export Hardware, " and make sure to check the "Include bitstream" option.
Part 2 Vitis designLaunch Vitis IDE and create a new application project. When prompted, select the XSA file exported from Vivado and create a new platform.
When we are creating new platform, it will setup the application properties for first ARM core. therefore, For the next steps, configure the platform to support FreeRTOS running on the A53 core.
For the boot component, select "Zynq MP FSBL" (First Stage Boot Loader) to set up the initial boot process for the Zynq UltraScale+ device.
Choose a name for your project and proceed to the next step. Since the first application will run on the A53 core, select FreeRTOS as the operating system.
Next, select an empty C application from the provided templates, this process will create our first application project.
Download the provided C files from the GitHub repository, copy the A53 main file, and paste it into the source folder of the project.
The driver code includes key sections such as address definitions and push-button control.
Address definitions are defined as macros for the base addresses of the two AXI GPIOs dedicated to the A53 core. These macros are derived from the x parameters header file and correspond to the hardware design in Vivado.
#define LED_GPIO_ADDR XPAR_ARM0_LED_CTRL_BASEADDR //0xA0000000
#define PUSHBUTTON_GPIO_ADDR XPAR_ARM0_SW_POLL_BASEADDR //0xA0010000
In the push-button control section, the program reads the state of first push button in polling mode. When the pushbutton is pressed, the program toggles the first LED and prints "Hello World" to the console.
while (1) {
// Read the pushbutton state
pushbutton_state = Xil_In32(PUSHBUTTON_GPIO_ADDR);
// Check if the pushbutton is pressed (assuming active-high logic)
if (pushbutton_state != 0) {
// Toggle the LED state
led_state ^= 1;
Xil_Out32(LED_GPIO_ADDR, led_state);
// Print "Hello World"
xil_printf("%d-Hello world from ARM0: A53_0 FreeRTOS\r\n",counter);
counter++;
} else {
//turn off the LED if push button is not pressed
Xil_Out32(LED_GPIO_ADDR, 0);
}
Please note that, The AXI GPIO peripherals are accessed using Xil_In and Xil_Out.
A 25-millisecond delay is added to ensure the LED blinking frequency is visible.
Once the application is ready, build the project. After the build is complete, the first ELF file will be generated for the A53 core.
To run another application on the R5 core, we need to create a new domain in the platform.
Open the platform SPR by double-clicking it.
You will see the existing FreeRTOS domain for the A53 core. Right-click on the platform and select “Add Domain.” Provide a name for the new domain, and from the OS dropdown, select "Standalone" as the operating system for the R5 core. From the processor dropdown, choose Cortex-R5-0 and click OK.
The platform will now be outdated. Build the platform to update it with the changes.
Second applicationAdd a second application by right-clicking on the application project and selecting “Add Application Project.”
Provide a name for the new application. In the application wizard, select Cortex-R5-0 as the target processor.
Since there is only one domain created for the R5 core, it will be automatically selected. Choose an empty C application from the templates and complete the wizard. Copy the main file for the R5 core from the provided resources and paste it into the source folder.
here you will find two applications: one FreeRTOS for A53 and one Standalone for R5:
The R5 main file has a similar structure to the A53 driver but uses base addresses for the third and fourth AXI GPIOs.
#define LED_GPIO_ADDR XPAR_ARM1_LED_CTRL_BASEADDR //0xA0020000
#define PUSHBUTTON_GPIO_ADDR XPAR_ARM1_SW_POLL_BASEADDR //0xA0030000
The R5 application reads the state of SW17 in polling mode. When SW17 is held, it toggles the DS39 LED and prints "Hello World" to the console.
DDR allocationOne common mistake when running two cores in Zynq devices is failing to adjust the DDR memory addresses in the linker scripts to ensure each application uses its own memory region.
By default, the linker scripts assign the same base address to both applications, with the memory size determined by the hardware platform's available DDR. If left unchanged, both the ARM0 and ARM1 applications will attempt to operate within the same DDR address space, which will cause conflicts.
To resolve this, the DDR memory regions must be divided so that each core operates in its own space. Open the linker script for each application, (found at Explorer > application > src > lscript.ld) and modify the ps7_ddr_0 values at the top.
For simplicity, I divided the DDR memory into two halves: the lower half for ARM1, and the upper half for ARM0. Since these are bare-metal applications performing minimal tasks like printing a single line to UART, they don’t require much memory.
ARM1’s base address was left at its default value of 0x100000, but its size was reduced to 1 Gig.
(half the available DDR). ARM0’s base address was set to start immediately after ARM1’s allocated region, and its size also set to 1 Gig.
With both applications configured, build the entire project. Once completed, you have the A53 application running FreeRTOS and the R5 application running as a standalone program. You can find that the second elf file is also created to run on the R5
With our software built, it's time to run it on the actual hardware. First, let's connect our ZCU104 board. We'll need to connect the power supply, the JTAG USB connection for programming, and the UART USB connection for console output.
Using the "lunch hardware" feature in Vitis, we'll load our bitstream on PL side and two applications onto the board.
Obtained results:Once the applications are launched, both ARM cores output their respective messages to the serial terminal, confirming that the two applications are successfully running on the targeted ARM cores.
Subsequently, the ARM cores begin monitoring the pushbuttons.
- When the first pushbutton is pressed, the A53 core starts blinking the first LED and prints "Hello World" from A53 to the serial terminal.
- Similarly, when the second pushbutton is pressed, the R5 core starts blinking the second LED and prints "Hello World" from R5 to the serial terminal.
Comments