Van Barco’s C naar Handel-C op FPGA

Author:

Herman Roebbers (herman.roebbers@tass.nl) is consultant bij Tass Software Professionals. Een deel van zijn tijd onderzoekt hij herconfigureerbare hardware en resource-constrained systemen. Hij is ook gedetacheerd bij Tomtom Eindhoven als development consultant. Tevens is hij lid van de Tass-stuurgroep Hardware/Software Interfacing.

Reading time: 7 minutes

De Handel-C-compiler maakt het mogelijk direct hardwarebeschrijvingen te genereren uit C-code. In diverse bijdragen aan Bits&Chips heeft Herman Roebbers de afgelopen jaren verslag gedaan van de ervaringen die Tass Software Professionals (voorheen Philips Tass) hiermee heeft opgedaan. Een update.

Sinds 2001 zet Tass Handel-C in om aan te tonen dat hiermee in korte tijd systemen zijn te bouwen die niet onderdoen voor systemen ontworpen in Verilog of VHDL. De technologie is te gebruiken om hardwareondersteuning te bieden voor softwareverwerking (coprocessing), maar ook om de complete systeemhardware te ontwikkelen. Prototyping van algoritmes is hiermee zeer goed mogelijk, maar Handel-C is ook inzetbaar voor een product.

In dit artikel laten we zien hoe gemakkelijk het is om op een FPGA een MPeg-4-encoder te realiseren die niet alleen snel genoeg is, maar ook van vergelijkbare grootte als een VHDL-implementatie. Ook demonstreren we het effect van compressie op de kwaliteit van het beeld na decodering. Daarbij gaan we uit van de C-broncode van Barco-Silex.

Discrete cosinustransformatie

Van de mensen van Barco-Silex heeft Tass C-code gekregen die bedoeld is om als verificatie te dienen van hun VHDL-code. Door middel van schuifoperaties, optellingen en opzoekacties in tabellen zijn hierin allerlei bewerkingen op variabelen geïmplementeerd, zoals de hardware dat ook doet. Dit maakt dat er geen extra werk nodig is om van drijvende komma over te gaan naar integer, en dat scheelt een hoop.

Figuur 1: Een encoder werkt in drie slagen: transformatie, kwantisatie en codering.

In het algemeen werkt een encoder in drie slagen (Figuur 1): transformatie, kwantisatie en codering. Transformatie is het omzetten van gegevens in een andere notatie zonder verlies van informatie. Kwantisatie is het beperken van de grootte van de representatie, zeg maar het aantal bits. Codering is hier het anders representeren van de informatie, met als doel nog minder bits te gebruiken. Een voorbeeld hiervan is run length-codering. De decoder is het spiegelbeeld van de encoder, en als de kwantisatieslag geen informatie heeft weggegooid, kunnen we het originele ingangssignaal weer reconstrueren. Het algemene doel is om de hoeveelheid uitvoer te minimaliseren zonder (merkbaar) verlies van informatie, en dat bereiken we door in alle slagen het meest geschikte algoritme te kiezen.

Het basisidee van MPeg-4-video is dat vastligt hoe de decoder zijn werk doet (hiervoor bestaat een referentie-implementatie). Wat de encoder allemaal doet en hoe is niet belangrijk, als er maar een output uitkomt die de decoder juist kan interpreteren. Dit concept maakt het mogelijk dat er vele verschillende implementaties kunnen bestaan met elk hun specifieke kwaliteiten, geoptimaliseerd op verschillende criteria (bijvoorbeeld snelheid, kwaliteit of gebruik van systeembronnen). MPeg-4 kent een aantal profielen, die onder meer te maken hebben met aspecten als diensten, kwaliteit en resoluties.

Voor MPeg-4 geldt ook dat we verschillende soorten invoer op verschillende manieren kunnen bewerken. Voor de transformatie hebben wij de discrete cosinustransformatie (DCT) gebruikt, omdat die voor camerabeelden de beste resultaten levert. De grote truc bij encoding is het variëren van de kwantisatie Q op zo‘n manier dat we zo veel mogelijk informatie kunnen weggooien zonder dat het na decodering opvalt. Voor de coderingsslag kiezen we ten slotte voor de Huffman-methode, omdat die het meest compacte resultaat geeft.

Figuur 2: Een signaal van het type YUV 4:2:0 bestaat uit vier Y-blokken van acht bij acht pixels en twee onderbemonsterde blokken U en V, waar de informatie van twee horizontale en twee verticale pixels is gemiddeld tot één nieuw beeldpunt.

Blok voor blok

Onze opdracht is het coderen van video met een resolutie van 720 bij 576 pixels en vijfentwintig volledige beelden per seconde (standaard tv-resolutie). Het te bouwen systeem moet realtime kunnen werken, wat wil zeggen dat we elk beeld in een vijfentwintigste seconde moeten coderen. Op dit moment doen we nog niet aan bewegingsschatting, omdat dit te veel inwerktijd vereist van de studenten die het project uitvoeren.

Het te bewerken signaal is van het type YUV 4:2:0. Y is het helderheidsignaal en U en V zijn de kleurverschilsignalen, die we horizontaal en verticaal een factor twee onderbemonsteren (Figuur 2). Pixels groeperen we in blokken van acht bij acht. Dan nemen we vier van deze blokken van Y en de twee onderbemonsterde blokken U en V samen tot een macroblok van zestien bij zestien pixels. Dit levert in totaal 256 + 64 + 64 = 384 8 bit waardes op. Zo‘n macroblok is de eenheid van bewerking in het compressiealgoritme.

Invoer van de MPeg-4-video-encoder is een stroom macroblokken (Figuur 3). Gegeven de resolutie van 720 bij 576 gaat het hier om 720 / 16 = 45 macroblokken horizontaal en 576 / 16 = 36 blokken verticaal. Dit resulteert in een totaal van 45 * 36 = 1620 macroblokken per beeld. Met een verversingssnelheid van vijfentwintig beelden per seconde moeten we dus 1620 * 25 = 40.500 macroblokken per seconde verwerken. Dit is 40.500 * 6 = 243.000 blokken per seconde. Er zijn dus 1 / 243.000 = 4,11 microseconden beschikbaar om één blok van acht bij acht pixels te verwerken.

Voor het project hebben we ervoor gekozen om de FPGA met een klokfrequentie van 50 MHz te laten werken. Dan komen 4,11 microseconden overeen met 205 klokcycli. In deze tijd moeten we dus alle bewerkingen doen.

Figuur 3: Een MPeg-4 Simple Profile I-frame-encoder heeft als invoer een stroom macroblokken.

De originele code leest een heel beeld uit een bestand en werkt vervolgens daarop. Bij implementatie op een FPGA zou dit echter te veel geheugen vereisen. Daarom werken we blok voor blok. Een ander probleem is dat alle stappen in de C-code van Barco sequentieel worden uitgevoerd. In Handel-C duurt elk statement echter één klokcyclus, terwijl er veel meer dan 205 statements zijn. De drie algemene encodercomponenten zijn wel duidelijk aanwijsbaar in de code. De eerste stap is dan om deze als een pijplijn achter elkaar te zetten. Ze kunnen dan allemaal tegelijk werken op een ander deel van de data. De verschillende slagen koppelen we via Fifo-buffers. Het deel dat er het langst over doet, bepaalt nu de prestatie van het geheel (Tabel 1).

Nul poorten

Het eerste systeem dat we bouwen (Figuur 4), bestaat uit een desktop-pc die is voorzien van een PCI-insteekkaart (RC2000) met daarop een Xilinx-FPGA van het type XC2V3000 (drie miljoen poorten). Naar de insteekkaart sturen we een invoerbestand via de Data Streaming Manager (DSM), een meegeleverde bibliotheek. De kaart bevat de DSM-interface naar de PCI-bus en de encoder, die de aangeleverde waarden omzet in een stroom bits. Die gegevens sturen we via de DSM weer terug naar de CPU, die ze opslaat in een bestand.

Het realtime karakter blijkt daaruit dat de file acht seconden aan informatie bevat die het systeem in ongeveer vijf seconden verwerkt. De correctheid van de implementatie bewijzen we door het gegenereerde bestand zonder artefacten af te spelen op een mediaspeler (VLC). Het hele systeem beslaat ongeveer 88 procent van de FPGA (zonder bewegingsschatter). De VHDL-versie van Barco-Silex gebruikt ongeveer 20 tot 25 procent, inclusief bewegingsschatter. Gebruik van systeembronnen heeft echter niet de aandacht in deze exercitie; eerst moet het werken.

Nu we een werkende opstelling hebben om als uitgangspunt te nemen, stellen we de eisen bij: doel is om minder systeembronnen te gebruiken en toch realtime te blijven. En als dat lukt, hebben we nog een grote wens: omdat de huidige opstelling nogal groot is om mee te nemen, willen we een draagbare demo, die bovendien ook nog leuker is om te zien en het effect van variatie van de Q direct zichtbaar kan maken. Student David Arntzenius krijgt de taak om dit te realiseren.

Figuur 4: Het eerste systeem dat Tass heeft gebouwd, bestaat uit een desktop-pc die is voorzien van een PCI-insteekkaart (RC2000) met daarop een Xilinx-FPGA van het type XC2V3000 (drie miljoen poorten).

Arntzenius ontdekt dat in de eerste implementatie een probleem zit dat in een eerdere versie niet bestond, waardoor de uitvoering van de kwantisatie nu veel te lang duurt (zie Tabel 1). Dit moesten we eerst oplossen, en dat hebben we gedaan door de kwantisatie op te splitsen in drie parallelle taken. Dit maakt deze slag niet merkbaar groter. Ook blijkt dat de eerder genoemde 88 procent poortgebruik voor een belangrijk deel bestaat uit de DSM-interface. Na correctie hiervoor neemt de eigenlijke encoder zo‘n 56 procent in beslag. Er valt dus nog een en ander te doen.

Als je moet gaan bezuinigen op poorten, moet je natuurlijk wel weten hoeveel je er in eerste instantie gebruikt. Dit lijkt simpel, maar is in de praktijk toch lastig. Dit komt door de optimizer van de compiler. Die laat alles weg dat niet nodig is. Een voorbeeld: stel je hebt een blokje A, met ingangssignalen B en C en uitgang D. Als je wilt weten hoe groot A wordt, zou je kunnen denken dat het voldoende is om een aanroep van A te doen met een vaste waarde voor B en C. Als je dan ook nog het resultaat D niet echt gebruikt, dan optimaliseert de compiler je hele blok A weg en heb je een grootte van nul poorten.

Module Klokcycli eerste implementatie
DCT 165
Kwantisatie 319 (na reparatie, voordien <205)
DC-predictie 110
AC-predictie 140
Zigzagscan 150
Entropie-encoder 135

Tabel 1: Executieduur van de verschillende fasen.

De enige manier om te garanderen dat de compiler geen weet kan hebben van invoer en uitvoer is om deze aan de pennen van de FPGA te leggen. Alleen dan kan de optimizer niets van tevoren weten en niets weggooien. De resulterende grootte is dus in feite een bovengrens. In de praktijk kan blijken dat de optimizer toch kans ziet om blok A nog te verkleinen, en dat is dan mooi meegenomen. Op basis van deze meetmethode hebben we bepaald waar de meeste poorten voor nodig zijn (Figuur 5).

Losse logica

Uit het meetresultaat blijkt duidelijk dat de meeste winst is te halen uit het optimaliseren van de discrete cosinustransformatie. De DCT voert twee keer dezelfde berekening uit op een acht bij acht pixel blok: de eerste keer met acht bits invoer en kolom voor kolom, de tweede keer met twaalf bits invoer en rij voor rij op het resultaat van de eerste berekening. Omdat we overal met integers rekenen, moeten we meer bits voor tussenresultaten gebruiken om de vereiste precisie te handhaven dan de initiële acht, namelijk veertien. Bij het terugbrengen van het poortgebruik zijn vier hoofdpunten in beschouwing genomen: hergebruik van functionaliteit, reductie van de hoeveelheid bits per variabele, gebruik van on-chip geheugen en beperking van algemene overhead.

Figuur 5: Verdeling van FPGA-poorten over functies.

De twee berekeningen beoordelend op het eerste punt, bleek de uitgangspositie van de oorspronkelijke implementatie te zijn het optimaliseren van elke berekening afzonderlijk. Uiteindelijk is het beter om de berekening in beide gevallen met dezelfde generieke functie uit te voeren en in het eerste geval de functie aan te roepen met 14 bit waarden in plaats van 8 bit door er simpelweg de benodigde 0-bits voor te plakken. Bij nadere inspectie van de bitbreedte van de gebruikte variabelen bleek dat nog een groot deel van de code gebruikmaakte van de originele 32 bit variabeledefinities uit de Ansi-C-referentiecode. Het zorgvuldig toewijzen van de daadwerkelijk benodigde hoeveelheid bits per variabele resulteert niet alleen in minder poorten voor de variabelen zelf, maar ook in kleinere implementaties van de operaties op deze variabelen. Om nog verdere reductie te bereiken, slaan we de tussenresultaten op in Block Ram in plaats van daar losse logica voor te gebruiken. Eindresultaat van de optimalisatie is een reductie van de hoeveelheid slices voor de DCT-slag van meer dan 4300 naar 1500. Dit brengt het totaal terug naar 36 procent FPGA-vulling.

Ook de verbinding tussen de modules gebruikt nogal wat systeembronnen. Dit komt doordat de eerste groep projectstudenten heeft bedacht dat het makkelijk is om modules afzonderlijk te testen als ze allemaal dezelfde breedte van variabelen gebruiken. Dit is wel zo, maar hier gaat toch stiekem veel verloren. Een nieuw testmechanisme verhelpt het testprobleem en zorgt ervoor dat de verbindingen tussen de componenten precies de goede breedte hebben. De hoeveelheid slices in gebruik voor de interfacing hebben we teruggebracht van 1730 naar 860 (geschat). Het totale gebruik door de encoder hebben we daarmee gereduceerd tot 29 procent van de FPGA.

Op knoppen drukken

Volgende opdracht is om de implementatie over te zetten naar een RC300-bord met daarop een XC2V6000-FPGA (zes miljoen poorten), een aantal knoppen en een lcd-display. Ook moet een analoog cameraatje de plaats innemen van het testbestand. Een notebook moet de demo-opstelling completeren (Figuur 6).

Aan deze omzetting zitten nog een paar haken en ogen. Niet in het Celoxica/Agility-stuk, dat liet zich probleemloos omzetten van het RC2000- naar het RC300-bord, maar in het gebruik van het cameradeel. Het analoge cameraatje aangesloten op een van de CVBS-ingangen van de RC300 levert de YUV-informatie in een andere volgorde aan dan het eerder gebruikte testbestand. Hierdoor moesten we een aantal beeldlijnen bufferen.

Toen we dat hadden gedaan, stuitten we op het volgende probleem. De aanname was dat er 205 klokcycli beschikbaar waren per blok, gebaseerd op het aantal blokken per seconde. Bij gebruik van de camera komen de blokken eigenlijk sneller, omdat een gedeelte van de tijd van elk tv-beeld is gereserveerd voor de verticale synchronisatie. Hierdoor blijken er effectief maar 189 klokcycli beschikbaar te zijn voor bewerking. Dit probleem hebben we opgelost door één beeld te bufferen en dan gelijkmatig verdeeld in de tijd te bewerken. Het alternatief is om alle slagen in minder dan 189 cycli te doen, maar dan moeten we de DCT nog verder versnellen. Gegeven de tijd hebben we dit niet onderzocht.

Figuur 6: De nieuwe demo-opstelling van Tass bestaat uit een analoge camera, een RC300-bord met daarop een XC2V6000-FPGA (zes miljoen poorten), een aantal knoppen en een lcd-display, en een notebook met een mediaspeler.

Als mediaspeler hebben we op de laptop MPlayer gebruikt. Tussen de USB-aansluiting en dit programma moesten we nog een stukje software maken. Dit bleek allemaal niet echt moeilijk te zijn, aangezien er een USB-driver voor zowel de RC300 als het notebook is meegeleverd. De hiermee te bereiken doorvoersnelheid van rond 40 Mbit per seconde is volkomen toereikend.

Om het effect van compressie op het gedecodeerde beeld zichtbaar te kunnen maken, kunnen we op een paar knoppen op de RC300 drukken om de compressie te beïnvloeden. De Q-parameter kunnen we tussen 1 en 31 instellen, waarbij 1 het laagste niveau van indikking is en 31 het hoogste. De standaard ingestelde waarde is 5. Normaal wordt Q gestuurd door een rate controller, die de mate van compressie zo instelt dat er een constante bitrate resulteert. Nu wordt die functie dus vervuld door op de knoppen te drukken.

Het lcd-schermpje toont de gebruiker wat de Q-waarde is, de bijbehorende bitrate (Mbit/s) en het aantal beelden per seconde (FPS). Ook is een kleine historie te zien van de bitrate. Het zou leuk zijn geweest om het TFT-aanraakscherm van de RC300 in te zetten voor de gebruikersinterface, maar dat is iets voor een volgende opdracht.

Ook een complex stuk Ansi-C-code kunnen we met Handel-C zonder grote problemen of grote wijzigingen omzetten in een werkend FPGA-systeem. De benodigde hoeveelheid systeembronnen is niet heel veel groter dan bij een handgeschreven VHDL-implementatie. Barco toonde zich verbaasd dat het ons gelukt is om de compressie realtime te doen en was ook tevreden met de hoeveelheid gebruikte bronnen. Het is zeker nog mogelijk verder te optimaliseren als meer ervaren architecten hiermee aan de slag gaan. Dit valt echter buiten de huidige scope. En we hebben er een interessante, makkelijk mee te nemen demonstratieopstelling bij.

Dankwoord

Dank aan alle studenten die de afgelopen tijd aan de diverse opdrachten hebben gewerkt, aan hun begeleiders van Fontys, begeleider Jack Sleuters van Tass en aan Barco-Silex, dat zijn referentiecode ter beschikking stelde en benieuwd was naar de resultaten. Ook hebben we dankbaar gebruikgemaakt van de hulp van Celoxica/Agility.