Bare-metal Application with Vitis
Create a software platform, understand the BSP and control an AXI GPIO with low-level and high-level drivers.
From XSA to C code
The engineer uses Vitis to configure and build software for a Zynq processor. The first step is to import the hardware platform from the XSA file produced by Vivado.
Bare-metal means that the program runs directly on the processor without a complete operating system such as Linux. In Vitis, the standalone domain provides a small runtime, a BSP and drivers, but it does not provide Linux processes, services or a scheduler.
To apply these concepts, this course provides an application called the UART LED console. It receives commands from a serial terminal through the UART protocol and controls an AXI GPIO connected to the LEDs. This program is the running example in the following sections.
A Vitis application uses three main elements.
| Element | Content |
|---|---|
| Platform | Hardware description imported from the XSA |
| Domain | Target processor, operating system and BSP |
| Application | C or C++ sources, linker script and build options |
The workflow consists of creating a platform, selecting a standalone domain, adding the application and running it on the board. The user sends commands through UART while AXI GPIO drives the LEDs.
Cross compilation
The compiler runs on the development computer. The generated program runs on an ARM processor on the board. This operation is cross compilation.
The ELF executable contains machine code, data and debug information. From Vitis, the engineer can request that it be loaded through JTAG, a hardware debug link, into initialized memory. Autonomous boot later requires a boot image stored on the board.
The BSP
The Board Support Package connects software to the hardware described by Vivado. It contains headers, libraries and drivers for the platform.
xparameters.h provides device identifiers and peripheral addresses. Names come from the Block Design. They change when an instance is renamed or the platform changes.
#include "xgpio.h"
#include "xparameters.h"
XGpio leds;
int init_leds(void)
{
int status;
status = XGpio_Initialize(&leds, XPAR_FPT_LED_BANK_DEVICE_ID);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
XGpio_SetDataDirection(&leds, 1, 0x0U);
XGpio_DiscreteWrite(&leds, 1, 0x0U);
return XST_SUCCESS;
}This structure keeps initialization separate from application logic. XGpio_Initialize associates the software object with the hardware instance. XGpio_SetDataDirection makes channel 1 an output. XGpio_DiscreteWrite sets the LED state.
Level 1 driver
The XGpio API is a level 1 driver. It uses an instance structure and checks initialization. It is suitable for most applications.
void write_led_pattern(u32 pattern)
{
XGpio_DiscreteWrite(&leds, 1, pattern & 0xFFU);
}The mask limits the value to eight LEDs. Use 0x0FU on a board with four LEDs.
Level 0 driver
To compare both driver levels, this course also provides a small UART-controlled stopwatch. This second application uses direct register access. The method avoids the instance structure but requires register offsets and a base address.
#include "xgpio_l.h"
void configure_led_registers(void)
{
XGpio_WriteReg(XPAR_ZCU_LED_BASEADDR, XGPIO_TRI_OFFSET, 0x0U);
XGpio_WriteReg(XPAR_ZCU_LED_BASEADDR, XGPIO_DATA_OFFSET, 0x0U);
}XGPIO_TRI_OFFSET controls direction. A zero bit selects output. XGPIO_DATA_OFFSET holds the value written to the pins.
Level 0 access is useful in short code, loaders or critical sections. Level 1 access is easier to read and supports several instances more cleanly.
The UART LED console loop
The UART LED console reads each received character and updates the light pattern. The + character moves the center to the right. The - character moves it to the left. Characters from 0 to 7 directly select one LED.
The small stopwatch introduced above follows another structure. Its loop reads buttons, applies debounce logic, interprets serial commands and updates a counter.
| Command | Effect |
|---|---|
g | Starts the stopwatch |
s | Stops the stopwatch |
r or c | Clears the elapsed time |
The loop is a simple state machine. The Boolean timer_is_running records whether the stopwatch is active. Input events change this state. UART output and LEDs show the current value.
Limits of a software delay
The teaching version of the small stopwatch uses an empty loop to produce a delay close to 10 ms. This shows the program sequence, but it is not an accurate time base.
The delay depends on processor frequency, compiler options and cache activity. A robust application uses a hardware timer and, when needed, an interrupt.
Initializing and cleaning up the platform
init_platform() and cleanup_platform() are not part of the C language. They are utility functions defined in the platform.c and platform.h files added to the application project.
init_platform() performs the target-specific preparation required before application logic. Depending on the processor and project options, its implementation may enable caches or configure a 16550 UART used as standard output. On a platform that needs neither operation, the function may do very little. Read its implementation rather than assuming that it initializes every peripheral.
cleanup_platform() performs the matching shutdown operations defined by the same file. In the usual standalone template, it disables caches. It does not automatically reset every GPIO, timer or controller used by the application.
Embedded software often remains in an infinite loop. Code placed after that loop never runs. In that case, cleanup_platform() is useful only on an early exit path, such as an initialization failure. Its use must follow the actual application lifecycle.
Debug on the target
The engineer selects the required operations in the run configuration. Vitis then programs the PL, initializes the PS, loads the ELF file and, when requested, stops in main. A serial terminal shows application messages.
Check peripheral failures in this order.
- Confirm that the XSA matches the latest hardware.
- Confirm the symbol used from
xparameters.h. - Check the driver return status.
- Check GPIO direction.
- Check the address in Address Editor.
- Check board pins and constraints.
Official references
The Vitis Embedded Software Development Guide UG1400 documents platforms, domains and applications. The AMD embeddedsw repository contains driver sources and examples.
Key points
The engineer imports hardware into Vitis through an XSA. The next choice is a domain that defines the processor, standalone environment and BSP. xparameters.h connects Block Design names to C code. Level 1 drivers provide a structured API. Level 0 drivers access registers directly.
Test your knowledge - Chapter quiz