I assume you know how to set-up the board diagram in Vivado. If not see this or this guide, the latter which is from Digilent. The Zedboard uses Zynq7000.Note I am a beginner with Ada myself so this code may not be the best way of doing things!
Project is available on git.
1.1 Setting up EMIO to Allow the Processing System to Control the LEDsOn a Zedboard, the LEDs can only be controlled via the Programmable Logic (PL) (FPGA) side, as the physical pins are only connected to the Field Programmable Gate Array (FPGA). As such, the CPU cannot directly control the LEDs. Instead, the CPU instructions must be routed through the PL, which can be done using the Extended Multiplexed Input/Output (EMIO).
To enable the EMIO:
Note: I did the below myself. Other guides exist such as here
- Open the Board diagram.
- Double click on the processing system.
- Select MIO configuration, I/O Peripherals and at the end open GPIO. Tick EMIO GPIO and in the box select 8.
- The Processing System (PS) should now have a new port entitled GPIO_0, with 3 sets of ports each of 8-bits (7:0).
- Right click on GPIO_0[7:0], and click on "Make External". A port called GPIO_O_0[7:0] will be created.
- We can rename the port by right clicking it and selecting "External Port Properties". In the box External Port Properties we write the new name.
- The name will now be updated in the board diagram.
- Inside sources, if not already done, right click on the ps.bd and select generate HDL wrapper. Let Vivado handle updates.
- Figure 1.1: The EMIO GPIO output from the PS is now routed to the FPGA via o_leds.
We now need to connect the LED pins to the o_leds port we just created. This port is an input to the FPGA, and thus an output from the PS. We will do this by writing a constraints file, which connects pins on the Zedboard to signals in our RTL design loaded onto the FPGA.
1.2.1 Constraints File Syntax- Vivado constraints files (.xdc) use Tcl syntax to assign physical and electrical properties to design objects such as ports. The general form is:
set_property <PROPERTY> <VALUE> [get_<object_type> <object_name>]- For top-level I/O, the object type is get_ports, where the port name must match the RTL design exactly. For example:
set_property PACKAGE_PIN T22 [get_ports { leds_o[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports { leds_o[0]}]- Multiple properties can be assigned in a single command using -dict:
set_property -dict { PACKAGE_PIN T22 IOSTANDARD LVCMOS33 } [ get_ports { leds_o[0]}]- Curly braces {} are used to group names (e.g. bus indices like leds_o[0]) and prevent Tcl parsing issues.
- Port names in get_ports must exactly match the top-level design (case-sensitive).
- PACKAGE_PIN and IOSTANDARD are device-specific properties defined by the FPGA and board.
- The order of constraints in the file is generally not important.
- Comments are written using #.
To write our tcl:
- Inside the project tree create a folder called constraints, and inside constraints a file entitled constraints.xdc.
- We next need to refer to the ZedBoard documentation at ZedBoard Hardware User Guide, in order to understand which LED is connected to what Zynq pin, and the correct electrical characteristics. This information is found in section 2.7.3 on page 20.
- We note that the LEDs are sourced from 3.3 V banks. This means each LED pin requiers the input output standard LCVMOSS 33. If we take Zynq pin T22 then, we have:
set_property PACKAGE_PIN T22 [ get_ports { o_leds[0]}]
set_property IOSTANDARD LVCMOS33 [ get_ports { o_leds[0]}]- The get_ports name and index is decided by us. We could have used a different name, e.g.:
set_property PACKAGE_PIN T22 [get_ports {leds_out[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {leds_out[0]}]- We can also using -dict to make it one line:
set_property -dict { PACKAGE_PIN T22 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[0]}]- The full constraints file for all 8 LEDs is therefore:
set_property -dict { PACKAGE_PIN T22 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[0]}];
set_property -dict { PACKAGE_PIN T21 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[1]}];
set_property -dict { PACKAGE_PIN U22 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[2]}];
set_property -dict { PACKAGE_PIN U21 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[3]}];
set_property -dict { PACKAGE_PIN V22 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[4]}];
set_property -dict { PACKAGE_PIN W22 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[5]}];
set_property -dict { PACKAGE_PIN U19 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[6]}];
set_property -dict { PACKAGE_PIN U14 IOSTANDARD LVCMOS33 } [ get_ports { o_leds[7]}];- Note, there is a zedboard_master.xdc that can somewhat simplify the above process.
- Next, add the constraints file to the project by selecting Add Sources inside Vivado.
- In the project tree make a folder called temp.
- Generate the bitstream. Once complete, select File -> Export->Export Hardware.
- Click continue, tick include bitstream and place inside a folder called temp.
We now need to configure the PS GPIO EMIO pins as outputs by writing the GPIO controller’s direction and output-enable registers, then write the data registers to drive the bits high or low.
From the Zynq 7000 SoC Technical Reference Manual (UG585), the introduction to section General Purpose I/O reads:
The general purpose I/O (GPIO) peripheral provides software with observation and control of up to 54 device pins via the MIO module. It also provides access to 64 inputs from the Programmable Logic (PL) and 128 outputs to the PL through the EMIO interface. The GPIO is organized into four banks of registers that group related interface signals. Each GPIO is independently and dynamically programmed as input, output, or interrupt sensing. Software can read all GPIO values within a bank using a single load instruction, or write data to one or more GPIOs (within a range of GPIOs) using a single store instruction. The GPIO control and status registers are memory mapped at base address 0xE000_A000.
The last senetence is of particular importance as it tells us the base address of the GPIO control and status regsiters we must work from. Further, the EMIO GPIOs are connected to banks 2 and 3
- The GPIO control and status registers are memory mapped at base address:
0xE000_A000- From our board diagram, we know that we are using EMIO GPIO_O(7:0) output port i.e. bits 7 downto 0 of the EMIO, which we have connected to the PL via the port o_leds.
- To send data to our LED, then, we must find in the register map (which is found under section Register Summary of UG585) the correct control registers to control GPIO_O.
- Since EMIO[0:31] is bank 2, we find the correct registers are
Thus, in software we must:
- Set direction output => Set bit 0 in DIRM_2
- Enable output => Set bit 0 in OEN_2
- Write value => Set/clear bit 0 in DATA_2
Note: I realised a bit too late (and I was lazy to change as I had the ADA code written) that I should´ve used the MASK_DATA_LSW register as it allows one to select specific bits to write to. All 32 bits of the Data register are written at one time.
1.3.1 Ada Code - zedboard_emio_gpio.ads- The specification file is used to set the base address for the EMIO GPIO, and the offsets.
- As this is memory-mapping, we do not use access types, which are in some respects the Ada version of pointers, and as far as I am aware should typically be avoided.
- Instead, we use what is called representation clausing, which, as I understand it, allows one to associate a variable (or enumeration type) to a specific memory location or hardware register at a fixed address, allowing the program to directly map variables onto physical memory (such as memory-mapped I/O registers) without using traditional pointers.
Note I use a lot of comments as I am learning the language myself. I also intentionally qualify functions as much as possible so I can learn which function (procedure) belongs to what package.
The.ads code is:
-- Package to control the EMIO GPIO Bank 2 of the Zedboard .
-- We are only able to control 8 LEDs .
-- As this is memory mapping , we use address binding via representation
-- clauses
-- (as opposed to Access types ).
-- Access Types , essentially pointers , are typically only utilised when the
-- address is dynamic (not known at compile time ).
-- I extensively use comments as I am learning as I go!
-- I intentionally qualify everything to understand which function is apart
-- of which package .
with System ; -- A top - level Ada package
with System . Storage_Elements ; -- A child package of Storage .
with Interfaces ; -- defines types with exact sizes
package zedboard_emio_gpio is
use System . Storage_Elements ; -- without get a compile error :
-- possible missing with /use of System .
-- Storage_Elements .
-- It is due to use of the + , which needs the
-- use clause I believe
procedure Initialise ;
procedure Set_LEDs ( Value : Interfaces . Unsigned_8 ) ;
private
-- The following is all private to hide the Hardware details .
-- Declare the GPIO Control register base address .
-- To_Address is a type conversion as
-- " Address " is a particular type and we have
-- a hex literal that has to be converted .
-- We use constant as the values are fixed by Hardware .
GPIO_Control_Reg_Base : constant System . Address :=
System . Storage_Elements . To_Address (16# E000_A000 #) ;
Data_2_Addr : constant System . Address :=
GPIO_Control_Reg_Base + System . Storage_Elements . Storage_Offset (16#48#) ;
DIRM_2_Addr : constant System . Address :=
GPIO_Control_Reg_Base + System . Storage_Elements . Storage_Offset (16#284#) ;
OEN_2_Addr : constant System . Address :=
GPIO_Control_Reg_Base + System . Storage_Elements . Storage_Offset (16#288#) ;
-- Now the representation clauses :
Data_2 : Interfaces . Unsigned_32 ;
for Data_2 ' Address use Data_2_Addr ;
pragma Volatile ( Data_2 ) ; -- Need this according to chat gpt
-- suppress any optimizations that would interfere
-- with the correct reading of the volatile variables .
DIRM_2 : Interfaces . Unsigned_32 ;
for DIRM_2 ' Address use DIRM_2_Addr ;
pragma Volatile ( DIRM_2 ) ;
OEN_2 : Interfaces . Unsigned_32 ;
for OEN_2 ' Address use OEN_2_Addr ;
pragma Volatile ( OEN_2 ) ;
end zedboard_emio_gpio ;1.3.2 Ada Code - zedboard_emio_gpio.adbThe main body.adb file is:
with Interfaces ; use Interfaces ; -- need the use clause to use or , + , and
-- operations etc.
-- compiler gives error otherwise .
package body zedboard_emio_gpio is
procedure Initialise is
begin
DIRM_2 := DIRM_2 or 16# FF #; -- Set bottom 8 bits to 1 via bitwise or
-- operation .
-- 1 indicates output .
-- We do this to not set or change any other
-- bits .
-- We could have used the MASK_DATA_LSW also
.
OEN_2 := OEN_2 or 16# FF #; -- 1 indicates output is enabled
end Initialise ;
procedure Set_LEDs ( Value : Interfaces . Unsigned_8 ) is
begin
Data_2 := ( Data_2 and not 16# FF #) or Interfaces . Unsigned_32 ( Value ) ;
end Set_LEDs ; -- Interfaces . Unsigned_32 ( Value )
-- is a type conversion
-- Converts 8 -bit Value to 32 bits
-- unsigned .
end zedboard_emio_gpio ;1.3.3 Ada Code - main.adb- In the main.adb file we import the Ada.Real_time package, which allows us to use the Seconds function and Clock functions to set the delay between the LEDs being on and off.
- Hex AA (0xAA) means that LEDs LD1, LD3, LD5 and LD7 will turn off and on, whilset the others are always off.
with zedboard_emio_gpio;
with Interfaces;
with Ada.Real_Time; use Ada.Real_Time;
procedure Main is
D : Time_Span := Seconds (5); -- D is of Type Time_Span, Seconds is a function.
-- Nanoseconds also exists for example
Next : Time := Clock + D; -- What is dif between clock and clock time?
begin
zedboard_emio_gpio.Initialise;
delay until Next;
Next := Next + D;
loop
zedboard_emio_gpio.Set_LEDs (16#AA#);
delay until Next;
Next := Next + D;
zedboard_emio_gpio.Set_LEDs (16#00#);
delay until Next;
Next := Next + D;
end loop;
end Main;1.3.4 Ada Code - blink_led.gpr- Finally I give the.gpr file. I started my project without using Alire. To make it an alr project, I cd’d into the project directory in my terminal and ran:
alr init --bin blink_led --in-place- This will create a.gpr file for you.
- "– in-place" tells Alire not to create a brand new folder (thanks ChatGPT).
- I then had to slightly update my.gpr file, as it was using the wrong file name for main (I changed it to "main.adb").
- Secondly, the sources was not pointing to where my main.adb was (the root). This was fixed by setting "." inside for Source_Dirs
The.gpr code is thus:
project Blink_Led is
for Runtime ("Ada") use "embedded-zynq7000";
for Target use "arm-eabi";
for Source_Dirs use (".","mng_pl_ps/","config/");
for Object_Dir use "obj/";
for Exec_Dir use "bin";
for Main use ("main.adb");
end Blink_Led;I manually added
for Runtime ("Ada") use "embedded-zynq7000";
for Target use "arm-eabi";- This is required. The "for Runtime" command tells the Gnat compiler what CPU/architecture to compile for, in this case the zynq700. Other options are avilable, such as light-zynq7000, however this does not include the Real_Time library and therefore Alire throws up an error.
- The "for Target" command tells the compiler which instruction set / toolchain to generate code for, in this case ARM using the EABI (Embedded Application Binary Interface). This ensures the compiled output is compatible with the ARM Cortex-A9 processor on the Zynq.
- The above was obtained from this link.
- Once this is all done, I ran "alr build" command.
- This should produce a.elf file in a folder called bin. It may simply be called main, I renamed it to make the extension explicit i.e. main.elf.
- Open Vitis
- Create a folder inside c: or somewhere with a short path name called workspace.
- Open Vitis. Select Open Workspace.
- Select File → New Component → Platform.
- Give it a name.
- Select Hardware Design and browse for the.tcl file.
- Use Operating System standalone.
- Select Generate Boot Artifacts.
- Finish and build platform.
- A platform should be built with Vitis.
- Once this is done, the bitstream can be downloaded to the Zedboard by selecting Vitis->Program Device. Make the sure the JTAG cable is connected of course.
We now need to download the main.elf file to run on the processing system. I did this using the Xilinx Software Command-Line Tool (XSCT). In the terminal do the following:
- Open a terminal and start XSCT:
xsct - Connect to the target board via JTAG:
connect- List available targets:
targets- You should see output similar to:
1 APU
2 ARM Cortex−A9 MPCore #0 ( Vec tor Catch )
3 ARM Cortex−A9 MPCore #1 ( Running )
4 xc7z020- Select the first ARM core (CPU 0):
targets 2- Reset the processor to ensure a clean state:
rst -processor- Download the compiled Ada executable (.elf) to memory, e.g., :
dow "C:/ path /to/ your / project /bin/ main.elf "Start execution:
conOnce the program is running:
The ARM Cortex-A9 begins executing the Ada application
- The ARM Cortex-A9 begins executing the Ada application
- The Main procedure is invoked via the GNAT runtime
- The LEDs connected via EMIO (LD1, LD3, LD5, LD7) should begin blinking according to the implemented logic
Project Complete.




_9nsOFQ7ama.png?auto=compress%2Cformat&w=48&h=48&fit=fill&bg=ffffff)




Comments