Complete Vitis tutorial: control LEDs over UART
Create a platform, standalone domain and Cortex-A53 application that controls the four ZCU104 user LEDs from a serial terminal.
This practical connects a ZCU104 UART terminal to its four user LEDs. Build a standalone application, load it through JTAG, and check two display modes.
+ and - move a variable-brightness pattern's center. 0 through 3 select fixed display and toggle the corresponding bit of its stored image.
Prepare hardware and XSA
This path uses Vitis 2022.2 and psu_cortexa53_0. Menus refer to that version; newer tools may organize platforms and applications differently.
First build the system in the Vivado course: ZCU104 PS, PS UART, four-output AXI GPIO connected to LEDs, clock, reset, and board constraints. Export hardware with bitstream as fpt_zcu104_base.xsa. This is the chosen project name, not an archive automatically supplied with the course.
| Element | Value used in this tutorial |
|---|---|
| Board | ZCU104 |
| Connection | USB JTAG and USB UART |
| Boot mode | JTAG |
| SW6 position | ON, ON, ON, ON |
| Processor | psu_cortexa53_0 |
| Operating system | standalone |
| Hardware description | fpt_zcu104_base.xsa |
| Serial rate | 115200 baud |
| LED GPIO | XPAR_FPT_LED_BANK_DEVICE_ID |
| Address in the supplied BSP | 0xA0010000 |
0xA0010000 is this design's chosen base. Assign it in Address Editor when following this map; software uses the BSP symbol rather than a hardcoded address. Check routing from the four bits to physical LEDs.
Create the platform
Launch Vitis and choose an empty workspace:
C:\zynq-workspaces\workspace_zynq_led
Choose Platform Project from Create New, name it zynq_led_platform, then select Create a new platform from hardware (XSA). Import your export and enter:
| Field | Value |
|---|---|
| Operating system | standalone |
| Processor | psu_cortexa53_0 |
| Boot components | Create the FSBL for the target processor |
After Finish, check the standalone domain targets Cortex-A53 number 0. If absent, review selected device and XSA.
Create the application
In Application Project, choose that platform:
| Field | Value |
|---|---|
| Application project name | zynq_led_app |
| System project | Create New |
| System project name | zynq_led_system |
| Processor | psu_cortexa53_0 |
| Domain | Platform standalone domain |
| Template | Empty Application (C) |
The application must use the domain BSP. Open xparameters.h and locate GPIO. For this hardware map, definitions resemble:
#define XPAR_FPT_LED_BANK_BASEADDR 0xA0010000U
#define XPAR_FPT_LED_BANK_HIGHADDR 0xA001FFFFU
#define XPAR_FPT_LED_BANK_DEVICE_ID 1UThe device ID can change after hardware edits; use the generated symbol. Check STDIN_BASEADDRESS, here 0xFF000000 for the selected PS UART. Configured baud rate must match the UART terminal.
Add the C program
Create src/main.c in zynq_led_app. This version checks GPIO initialization and reads UART without blocking refresh:
#include "platform.h"
#include "xgpio.h"
#include "xparameters.h"
#include "xstatus.h"
#include "xuartps_hw.h"
#include "xil_printf.h"
#define LED_CHANNEL 1U
#define LED_COUNT 4U
#define PWM_STEPS 32U
typedef enum {
DISPLAY_HOLD,
DISPLAY_FADE
} DisplayMode;
static XGpio user_leds;
static const u32 brightness_masks[LED_COUNT] = {
0xFFFFFFFFU,
0x55555555U,
0x11111111U,
0x01010101U
};
static
init_platform() runs preparation in platform.c; it does not replace initialize_led_output(). Direction bits zero configure the GPIO channel as output.
cleanup_platform() handles initialization failure. The main loop is infinite, so cleanup after it would never execute.
DISPLAY_FADE uses 32 software PWM phases. The masks provide 100%, 50%, 25%, and 12.5% duty according to distance from the center. DISPLAY_HOLD keeps a separate binary image initially off; digits toggle its bits. Returning through fade and back to hold restores that stored image.
Build and prepare the board
Save and run Build Project in Debug configuration:
text data bss dec hex filename
... ... ... ... ... zynq_led_app.elf
Build FinishedA missing GPIO symbol requires checking instance name, XSA, and BSP regeneration. UART errors require xuartps_hw.h and STDIN_BASEADDRESS. Explicitly rebuild after fixes.
With power off, connect JTAG and UART USB cables. Set SW6 to ON, ON, ON, ON for JTAG, then power on. Identify the PS serial port among available ports. ZCU104 UG1267 documents boot modes and connections.
Load and open the console
In Run As > Run Configurations, create Single Application Debug. In Target Setup, check PS initialization, bitstream, and Program FPGA or Program Device, then Run to load ELF.
Open Vitis Serial Terminal through Window > Show View > Xilinx. Connect the correct port at 115200 baud, eight data bits, no parity, one stop bit, and no flow control. PS settings must match.
Zynq LED console ready
Use +, -, or a digit from 0 to 3Check commands
| Command | Expected result |
|---|---|
+ | Move the pattern center to the next LED |
- | Move the pattern center to the previous LED |
0 to 3 | Toggle the selected LED |
x | Print Unsupported command |
The center stays within 0..3; moving beyond an endpoint does not wrap. Digits modify the stored fixed image rather than freeze a PWM instant.
For a blank console, check port, baud, JTAG mode, and loading. For a working console with inactive LEDs, check GPIO symbol, bitstream, channel, address, and constraints. Unexpected LED order requires comparing logical bits with physical wiring.
short_pause depends on processor and optimization. Changing its bound adjusts PWM speed without providing accurate timing. UART printing can also disturb refresh; use a timer or hardware PWM for stable behavior.
Extend the practical
Replace delay with a timer, add c to clear LEDs and ? to display mode. Measure refresh with ILA, then split console, LED output, and main into files while preserving behavior.
Test your knowledge - Chapter quiz