Kampis Elektroecke

Processing System und GPIO

Der ZYNQ kombiniert ein FPGA und einen Dual Core ARM Prozessor. Beide Komponenten können unabhängig voneinander programmieren werden, um sie so beliebig anpassen und einsetzen zu können. In diesem Artikel zeige ich, wie das Processing System (PS) des ZYNQ genutzt werden kann und wie man mit dem PS auf das FPGA zugreift.

Erstellen des Blockschaltbildes:

Zu allererst wird in Vivado ein neues Projekt erstellt. In dem geöffneten Projekt wird über das Menü Create Block Design ein neues Blockschaltbild mit dem Namen System erstellt:

Jetzt können mit einem Klick auf Add IP folgende IP-Blöcke eingefügt werden:

Anzahl Block
2x AXI GPIO
1x ZYNQ7 Processing System

Durch einen Klick auf einen der Blöcke erscheint links unten, neben dem Blockschaltbild, ein Fenster, in dem die Blöcke umbenannt werden können. Auf diese Weise werden die GPIO-Blöcke in LED und Schalter umbenannt.

Im nächsten Schritt wird mit einem Klick auf Run Connection Automation am oberen Rand des Blockschaltbild, ein Assistent gestartet, welcher die einzelnen Blöcke miteinander verbindet. Als letztes werden die Verbindungen der GPIO-Blöcke nach außen geführt, indem mit der rechten Maustaste auf das Verbindungsstück des Anschlusses GPIO geklickt und anschließend der Menüpunkt Make External ausgewählt wird:

Als nächstes wird das Processing System konfiguriert, indem das Konfigurationsmenü mit einen Doppelklick auf den ZYNQ7 Processing System-Block geöffnet wird. Über Import XPS Settings wird mit Hilfe der Konfigurationsdatei System.xml die Erstkonfiguration des Processing Systems durchgeführt:

Nachdem die Einstellungen geladen worden sind, können einige unnötige Komponenten wieder entfernt werden.

Menü Komponente
MIO Configuration Memory Interfaces Quad SPI Flash
MIO Configuration I/O Peripherals ENET 0, USB 0, SD 0, I2C 0, USART 0

Mit einem Klick auf OK wird die Änderung bestätigt. Das Processing System nimmt im Designer nun die folgende Form an:

Danach wird einer der beiden GPIO-Blöcke konfiguriert. In dem Konfigurationsmenü wird der Wert GPIO Width auf 4 gesetzt, wodurch der GPIO-Block einen 4-Bit breiten Ein- bzw. Ausgabeport führt.

Diese Schritte werden anschließend für den zweiten GPIO-Block wiederholt. Zuletzt benenne ich die Ausgangssignale noch um. Die fertige Schaltung sieht jetzt folgendermaßen aus:

Das Design wird anschließend gespeichert und nach einem Klick auf Project Manager wird das Menü Sources des Projektes geöffnet. Im Projektmanager wird dann die Datei System.bd ausgewählt und mit einem Rechtsklick das Menü geöffnet:

Es werden nun die Punkte Generate Output Products… und Create HDL Wrapper… ausgeführt um einen HDL-Wrapper für das Blockschaltbild zu erzeugen. Dieser Wrapper ist für die Erstellung des Bitstreams notwendig, da er das oberste Design festlegt (ähnlich wie die Funktion main in C) und zudem die Verbindung zwischen Peripherie und Prozessor beschreibt.

Sobald der HDL Wrapper erstellt wurde, kann über Run Synthesis die Hardwaresynthese gestartet werden. Dieser Vorgang dauert etwa 3-4 Minuten und in dem sich danach öffnenden Fenster wird dann auf Open Synthesized Design geklickt um das IO Planing zu starten:

Über das Menü WindowI/O Ports wird das Planungsmenü geöffnet, über das die Signale, welche in dem Blockschaltbild nach außen geführt wurden, den I/Os des FPGAs zugeordnet werden können.

Die Zuweisung wird gespeichert und anschließend mit einem Klick auf Generate Bitstream der Bitstream erzeugt. Sobald die Generierung des Bitstreams abgeschlossen ist, wird die sich öffnende Meldung mit OK bestätigt. Nun geht es an die Software für den ARM Prozessor!

Die passende Software:

Im ersten Schritt wird die Hardware, also der Bitstream, für das SDK exportiert, indem der Punkt Export aus dem Menü File ausgeführt wird. Dabei nicht vergessen den Haken bei Include bitstream zu setzen.

Über den Button Tools → Launch Vitis wird die Entwicklungsumgebung für die Software gestartet gestartet. Sobald das SDK geöffnet und ein Verzeichnis für den Arbeitsplatz ausgewählt worden ist, wird über FileNewApplication Project ein neues Anwendungsprojekt angelegt.

Durch einen Klick auf Next gelangt man ins nächste Auswahlmenü, wo die Zielplattform definiert werden muss. Hier wird in dem Reiter Create a new platform from hardware (XSA) die exportierte Hardware ausgewählt und die Eingabe über Next bestätigt.

Das darauf folgende Menü wird ebenfalls über Next geschlossen und zum Abschluss wird das Template Empty Application ausgewählt und die Einrichtung über Finish abgeschlossen.

In dem angelegten Projekt wird das Verzeichnis src ausgewählt und über File → New → Source File wird dann eine neue C-Datei mit dem Namen main.c erstellt:

In diese Datei müssen zuerst die notwendigen Headerdateien eingefügt werden:

#include "xparameters.h"
#include "xgpio.h"

Die erste Headerdatei beinhaltet die IDs und Adressen jedes IP-Cores die in dem Design integriert sind. Mit der zweiten Headerdatei werden spezielle Funktionen für den AXI GPIO-Block eingefügt.

Bevor ein IP-Block verwendet werden kann, muss dieser initialisiert werden. Dazu bringt jeder Treiber eine entsprechende XGpio_CfgInitialize-Funktion mit, die einen Zeiger auf die Konfiguration, die Adresse des jeweiligen IP-Clocks und einen Zeiger auf ein entsprechendes Objekt erwartet. Für den AXI GPIO-Block sieht die Initialisierung folgendermaßen aus:

ConfigPtr = XGpio_LookupConfig(XPAR_GPIO_0_DEVICE_ID);
if(ConfigPtr == NULL)
{
	xil_printf("[ERROR] Invalid GPIO switch configuration!\n\r");
	return XST_FAILURE;
}

Status = XGpio_CfgInitialize(&Switches, ConfigPtr, ConfigPtr->BaseAddress);
if(Status != XST_SUCCESS)
{
	xil_printf("[ERROR] Can not initialize switches!\n\r");
	return XST_FAILURE;
}

Bei der Variable ConfigPtr handelt es sich um einen Zeiger vom Typ XGpio_Config, der auf die aktuelle Konfiguration des jeweiligen GPIO-Blocks zeigt. Die Variable Instanz stellt ein Objekt vom Typ XGpio dar. Dieses Objekt wird bei jedem weiteren Funktionsaufruf für einen GPIO-Block mit angegeben. Die Variable ID ist die ID des jeweiligen Blockes. Sie dient der Identifikation und ist in der Datei xparameters.h zu finden.

/* Definitions for peripheral LED */
#define XPAR_LED_BASEADDR 0x41210000
#define XPAR_LED_HIGHADDR 0x4121FFFF
#define XPAR_LED_DEVICE_ID 0
#define XPAR_LED_INTERRUPT_PRESENT 0
#define XPAR_LED_IS_DUAL 0


/* Definitions for peripheral SCHALTER */
#define XPAR_SCHALTER_BASEADDR 0x41200000
#define XPAR_SCHALTER_HIGHADDR 0x4120FFFF
#define XPAR_SCHALTER_DEVICE_ID 1
#define XPAR_SCHALTER_INTERRUPT_PRESENT 0
#define XPAR_SCHALTER_IS_DUAL 0

Die Codezeilen zum initialisieren der beiden GPIO-Blöcke sieht damit folgendermaßen aus:

XGpio_Config* ConfigPtr;
XGpio Switches;
XGpio LED;

ConfigPtr = XGpio_LookupConfig(XPAR_GPIO_0_DEVICE_ID);
if(ConfigPtr == NULL)
{
	xil_printf("[ERROR] Invalid GPIO switch configuration!\n\r");
	return XST_FAILURE;
}

Status = XGpio_CfgInitialize(&Switches, ConfigPtr, ConfigPtr->BaseAddress);
if(Status != XST_SUCCESS)
{
	xil_printf("[ERROR] Can not initialize switches!\n\r");
	return XST_FAILURE;
}

ConfigPtr = XGpio_LookupConfig(XPAR_GPIO_1_DEVICE_ID);
if(ConfigPtr == NULL)
{
	xil_printf("[ERROR] Invalid GPIO LED configuration!\n\r");
	return XST_FAILURE;
}

Status = XGpio_CfgInitialize(&LED, ConfigPtr, ConfigPtr->BaseAddress);
if(Status != XST_SUCCESS)
{
	xil_printf("[ERROR] Can not initialize LED!\n\r");
	return XST_FAILURE;
}

Im nächsten Schritt müssen die Blöcke als Ein- bzw. Ausgang gesetzt werden.

XGpio_SetDataDirection(Instanz, Kanal, Direction);

Dabei ist die Variable Instanz der vorhin initialisierte GPIO-Block und die Variable Kanal der verwendete Kanal des GPIO-Blocks. Jeder GPIO-Block verfügt über zwei Kanäle, wobei in diesem Beispiel nur der Erste verwendet wird.

Über den Parameter Direction wird ein Pin als Eingang oder als Ausgang geschaltet. Der Wert 1 setzt einen Pin als Ausgang und der Wert 0 setzt den Pin dann als Eingang:

XGpio_SetDataDirection(&Switches, 1, 0xFFFF);
XGpio_SetDataDirection(&LED, 1, 0x00);

Die GPIO-Blöcke sind nun fertig konfiguriert. Mit der nachfolgenden Funktion kann der Status der Eingänge abgefragt werden. was wir in einer while()-Schleife auch direkt machen:

Data = XGpio_DiscreteRead(&Switches, 1);

Auf ähnliche Weise können die Ausgänge des GPIO-Blocks gesetzt werden:

XGpio_DiscreteWrite(&LED, 1, Data);

Zur Übersicht noch einmal das komplette Programm.

#include "xgpio.h"
#include "xil_printf.h"
#include "xparameters.h"

XGpio_Config* ConfigPtr;
XGpio Switches;
XGpio LED;
u32 Data;
u32 Status;

int main(void)
{
	xil_printf("[INFO] Processing System and GPIO example\n\r");

	ConfigPtr = XGpio_LookupConfig(XPAR_GPIO_0_DEVICE_ID);
	if(ConfigPtr == NULL)
	{
		xil_printf("[ERROR] Invalid GPIO switch configuration!\n\r");
		return XST_FAILURE;
	}

	Status = XGpio_CfgInitialize(&Switches, ConfigPtr, ConfigPtr->BaseAddress);
	if(Status != XST_SUCCESS)
	{
		xil_printf("[ERROR] Can not initialize switches!\n\r");
		return XST_FAILURE;
	}

	ConfigPtr = XGpio_LookupConfig(XPAR_GPIO_1_DEVICE_ID);
	if(ConfigPtr == NULL)
	{
		xil_printf("[ERROR] Invalid GPIO LED configuration!\n\r");
		return XST_FAILURE;
	}

	Status = XGpio_CfgInitialize(&LED, ConfigPtr, ConfigPtr->BaseAddress);
	if(Status != XST_SUCCESS)
	{
		xil_printf("[ERROR] Can not initialize LED!\n\r");
		return XST_FAILURE;
	}

	XGpio_SetDataDirection(&Switches, 1, 0xFFFF);
	XGpio_SetDataDirection(&LED, 1, 0x00);

	while(1)
	{
		Data = XGpio_DiscreteRead(&Switches, 1);
		XGpio_DiscreteWrite(&LED, 1, Data);
	}
	
	return XST_SUCCESS;
}

Das Programm muss abschließend noch kompiliert werden. Wird das System Projekt LED_system kompiliert, so wird während des Kompilierens ein fertiges Image erzeugt, welches direkt auf eine SD-Karte oder in den Flash-Speicher des ZYBO kopiert werden kann. Wird hingegen das Projekt LED kompiliert, so wird nur die aktuelle Anwendung erzeugt.

Zum Schluss kann wird das kompilierte Projekt auf das ZYBO übertragen werden. Dazu wird das Projekt im Project Explorer ausgewählt und anschließend das Menü RunRun Configurations ausgewählt. Danach wird durch einen Doppelklick auf Xilinx C/C++ application (System Debugger) ein neuer Eintrag angelegt.

Über den Button Run werden die Einstellungen anschließend ausgeführt. Es wird ein Reset des kompletten Systems durchgeführt und anschließend werden der Prozessor und das FPGA programmiert, sowie die Anwendung ausgeführt.

Das komplette Projekt steht in meinem GitHub-Repository zum Download bereit.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert