Xilinx

Bildverarbeitung in programmierbarer Logik mit C/C++

25. Februar 2016, 11:34 Uhr | Von Olivier Tremois, DSP-Spezialist FAE bei Xilinx
Diesen Artikel anhören

Fortsetzung des Artikels von Teil 1

Verlagern einer Funktion in die Hardware

Das erste Ziel der Beschleunigung besteht darin, jeden Taktzyklus ein neues Sample zu verarbeiten. Optimierung des Codes und Umbau des Interfaces erzielen weitere Beschleunigungen. Selbst wenn die Taktrate der programmierbaren Logik (PL) weit niedriger ist als die des Prozessorsystems (PS), bewirkt die Verarbeitung eines Pixels pro Takt eine erhebliche Beschleunigung.
Die Medianfilterung ist die einzige Funktion, die in Hardware verlagert wird. Obwohl die SDSoC-Umgebung dies mit nur einem Klick auf den Project Explorer sehr vereinfacht, fügt sie, um die Performance zu verbessern, keine Direktive (ausgenommen am Interface) hinzu oder verändert eine Codezeile. Derartige Modifizierungen bleiben die Aufgabe des Embedded-Programmierers. Das erklärt auch, warum die erste Beschleunigung im Allgemeinen nicht besonders dramatisch ist.
Die oben spezifizierte Funktion erstreckt sich mit zwei verschachtelten Schleifen auf das gesamte Bild. Sub-Loops gehen durch das strukturierende Element und sortieren alle Elemente. In unserem Beispiel verwenden wir einen Standard-Bubblesort-Algorithmus. Er ist zur Hardware-Implementierung besser geeignet als andere, weniger komplexe Algorithmen für eine Implementierung im Mikroprozessor:
for ( i=0; i<HeightOfImage; i++)
for ( j=0; j<WidthOfImage; j++)
{
Some Code
for ( s=0; s<NumberOfStages; s++)
for ( k=0; k<HeightOfStructElem; k++)
for ( l=0; l<WidthOfStructElem; l++)
{
Swap pixels if not correctly ordered
}
}
Da wir jeweils einen Pixel des Ausgangsbildes pro Taktzyklus verarbeiten wollen, müssen wir eine Direktive hinzufügen, um mit jedem Taktimpuls einen Pixelvektor-Sort zu starten. Dazu strukturieren wir die zweite Schleife (second loop) als Pipeline und bearbeiten mit einem Initiationsintervall (II) von 1 die Spalten des Bildes. (II ist die Anzahl der benötigten Taktzyklen, bevor eine neue Iteration beginnt.) Mit dieser Direktive entfaltet das SDSoC-Environment automatisch die verbleibenden inneren Schleifen und ermöglicht der Hardware die parallele Verarbeitung aller Iterationen.
Bildverarbeitungs-Algorithmen für Single-Core-Prozessoren sind recht einfach zu kodieren, weil diverse Prozessorfunktionen einen flüssigen Datentransfer zwischen externem Speicher und Prozessor erlauben. Die Caches L1 und L2 speichern Daten, die später benötigt werden. Das verbessert die Latenz.
FPGAs bieten von Natur aus keinen solchen Mechanismus. Das verhindert zwar die Nutzung desselben C/C++-Quellcodes für die Erzeugung des Hardware-Beschleunigers, führt uns aber zum Entwurf eines Caches passender Größe und Performance für unsere Applikation. Dies ist ein gutes Beispiel für die erforderlichen Änderung des C/C++-Quellcodes – nicht um die Funktionalität zu erhalten, sondern um die Performance so weit zu erhöhen, dass sie unseren Anforderungen entspricht. Die Xilinx Vivado High-Level Synthesis (HLS), die als SDSoC-Engine fungiert, generiert aus dem C/C++-Code ein RTL-IP (Register-Transfer-Level) und liefert damit eine Hardware-Architektur, die auf unseren Code auf Basis aller Direktiven angepasst ist.
Deshalb werden auch die Line-Buffer und Analysefenster bei der Analyse des Bildverarbeitungscode nicht automatisch generiert. Vivado HLS bleibt so nahe wie möglich am geschriebenen wCode. Das verhindert Optimierungen, die ohne Zustimmung des Entwicklers ausgeführt werden.
Entwickler, die mit Hardware-Bildverarbeitung vertraut sind, kennen natürlich auch Line-Buffer und Analysefenster. Damit nicht dasselbe Pixel aus dem externen Speicher mehrfach gelesen wird, werden Pixel temporär im internen Speicher (Block RAM) abgelegt und überschrieben, wenn sie nicht mehr gebraucht werden. Die Block-RAMs haben zwei Ports, die sich für Memory-Reads, -Writes oder beides eignen. Wenn der Accelerator einen Pixel akzeptiert, der zur Zeile L und Spalte C gehört, werden alle Pixel der Spalte C und Zeile (L-1 … L-6) aus dem Line-Buffer gelesen und an einem anderen Speicherplatz abgelegt (Bild 3). Um das Ziel »1 Pixel/Takt« zu erreichen, muss der gesamte Datentransfer in einem Taktzyklus ablaufen.
Außerdem muss in einem Taktzyklus auf alle benachbarten Pixel und das strukturierende Element zugegriffen werden. Deswegen definieren wir ein Analysefenster, das die betreffenden Pixel enthält und von Pixel zu Pixel variiert. In der SDSoC-Umgebung und in VHLS ist der Code nicht zeitlich festgelegt; das Tool parallelisiert alles, was sich in Bezug auf die Ressource und die Direktiven parallelisieren lässt. In unseren Code für die Batch-Verarbeitung fügen wir den Line-Buffer und das Analysefenster ein, indem wir zwei Arrays mit den passenden Partitionierungs-Direktiven deklarieren (Bild 4). Danach beschreiben wir die Datenbewegung als Lese/Schreib-Zugriffe auf diese Arrays (Bild 5).
Da sie auf dem Datenzugriff in einem Array beruht, kann die Pixel-Sortierung in Hardware recht komplex werden. Der C-Code einer Software-Implementierung sortiert den Vektor des Pixels, der durch das strukturierende Element validiert wurde, mit einem regulären Bubblesort. Die Komplexität dieses Algorithmus ist proportional dem Quadrat der Pixelzahl des strukturierenden Elements – in unserem Beispiel bis zu (7 x 7)². Es gibt effizientere Algorithmen, allerdings nur für größere Vektoren.
In Hardware-Implementierungen muss die Architektur für den Worst-Case ausgelegt werden. Wenn wir unser Ziel »1 Pixel/Takt« erreichen wollen, müssen wir eine sehr reguläre Struktur implementieren. Dazu spezifizieren wir einen Vektor der maximalen Größe 7 x 7 und alle nicht-validierten Pixel mit dem Wert Null, so dass sie am unteren Ende des sortierten Vektors liegen. Wir dimensionieren außerdem die Zahl der Stufen für den Worst-Case, auch wenn diese Zahl für ein strukturierendes Element mit weniger aktiven Pixeln niedriger wäre. Die Parallelisierung der Stufen kann nur dann erfolgen, wenn nicht auf jeder Stufe derselbe Vektor verwendet wird. Das resultiert in einem Array, in das der initiale Vektor beim Spalten-Index 0 eintritt und beim Spalten-Index 7 x 7 = 49 austritt (Bild 6).



  1. Bildverarbeitung in programmierbarer Logik mit C/C++
  2. Verlagern einer Funktion in die Hardware
  3. SDSoC-Compiler

Lesen Sie mehr zum Thema


Das könnte Sie auch interessieren

Jetzt kostenfreie Newsletter bestellen!

Weitere Artikel zu XILINX GmbH

Weitere Artikel zu Programmierbare Logik-ICs