Henk Muller is principal technologist bij XMos in Bristol. Op 18 juni geeft Muller een presentatie over dit onderwerp op de Bits&Chips Hardware Conference.

17 April 2009

Om de ontwikkeling van een complex realtime systeem beheersbaar te houden, delen ontwikkelaars dit vaak op in subsystemen, zoals verschillende microcontrollers en taakspecifieke chips. XMos uit Bristol heeft een processorarchitectuur die al deze subsystemen kan herbergen in aparte threads. Realtime multithreading en multicore zijn ingebakken in de instructieset. Henk Muller van XMos legt uit hoe dat werkt.

Ontwerpers benaderen een realtime systeem vaak als een verzameling subsystemen die allemaal een stukje van de realtimepuzzel oplossen: een of meer microcontrollers, een aantal logicacomponenten – al dan niet geïmplementeerd in een kleine CPLD – en verschillende taakspecifieke chips, bijvoorbeeld voor het aansturen van een lcd-scherm en een paar Phy‘s om externe signalen aan te sturen. Het grootste voordeel van deze benadering is dat elk subsysteem als een onafhankelijk realtime systeem kan worden beschouwd. Dit maakt het ontwikkelen aanzienlijk simpeler. De ene microcontroller kan bijvoorbeeld geluid produceren, de andere kan een radiochip aansturen en een DSP kan pre-distortion-algoritmes implementeren. Gezamenlijk vormen ze een realtimepijplijn.

Deze aanpak heeft echter ook nadelen. Het grootste probleem is dat alle subsystemen op een goed moment moeten worden geïntegreerd, wat ontwikkeltijd vergt. De ontwerper moet beslissen welke protocollen het beste zijn om de subsystemen met elkaar te verbinden (bijvoorbeeld I2S of RS232 serieel op 3,3 V) en hoe die protocollen aan beide kanten worden geïmplementeerd. Ook jaagt deze aanpak de productiekosten op, omdat er extra logica nodig is op de PCB.

Als we dit complexe systeem echter op één enkele processor willen implementeren, dan moet die de juiste realtimekarakteristieken geven aan de taken. Dit kan lastig zijn, vooral als de taken korte responstijden hebben. Bij XMos hebben we een processor ontwikkeld die zelf een aantal onafhankelijke taken tegelijk kan uitvoeren en die invoer- en uitvoerpinnen realtime kan aansturen. Dat betekent dat de verzameling microcontrollers, de externe logica en een aantal taakspecifieke chips kunnen worden geïntegreerd in één enkel device.

De XMos-architectuur is gebaseerd op een combinatie van technieken. Met multithreading zijn meerdere realtime taken op een core te draaien, elk met een korte responstijd. De processor is eventgedreven, om ervoor te zorgen dat een enkele thread meerdere realtime taken aankan met een iets langere responstijd. Dat levert absolute voorspelbaarheid op. Door een multicoreaanpak, met een netwerkarchitectuur om het systeem te kunnen opschalen, is het aantal taken niet gelimiteerd tot wat op een enkele processor past.

Techwatch Books: ASML Architects

Schaduwregisters

Multithreading is een term die meestal wordt gebruikt om een systeem te beschrijven dat meerdere taken ’tegelijk‘ kan draaien. In werkelijkheid laat het besturingssysteem elke taak om de beurt eventjes op de processor lopen. Multithreading wordt vaak ingezet als de programmeur onafhankelijke taken moet beschrijven. In Java bijvoorbeeld worden threads veel gebruikt om de interface met de gebruiker te programmeren en in netwerksystemen zijn threads geliefd om lagen van het Osi-model op te bouwen.

In een realtime omgeving kan multithreading een goede programmeertechniek zijn, mits er de garantie is dat een thread wordt gescheduled zodra deze een actie moet uitvoeren. In de XMos-architectuur bereiken we dit door threads iedere klokcyclus te schedulen: met een 400 MHz klok en acht threads, loopt iedere thread op 50 MHz. Vier threads op 400 MHz krijgen allemaal een snelheid van 100 MHz. In het laatste voorbeeld kan een enkele thread nooit sneller lopen dan met honderd miljoen instructies per seconde, maar willekeurig welke thread is dan ook met een vertraging van maximaal 10 ns weer aan de beurt. In plaats van een interruptroutine (met alle problemen van dubbele interrupts) gebruikt de programmeur een thread voor iedere realtime taak en laat die wachten op de volgende ’realtime-interrupt‘. Vanuit de architectuur gezien heeft elke thread zijn eigen registers die informatie specifiek voor die realtime taak opslaan, in plaats van een verzameling schaduwregisters om de toestand op te bergen tijdens een interrupt.

Omdat de reactietijd zo kort is, kan een thread in software veel taken uitvoeren waar normaal speciale hardware voor nodig is. Veel standaard I/O-protocollen, zoals Ethernet, I2C, S/PDif en SPI zijn volledig als een stuk software te beschrijven. Dit maakt een deel van de externe logica overbodig. Het betekent ook dat complexere protocollen (zoals Ethernet) eenvoudig kunnen worden veranderd om bijvoorbeeld kloksynchronisatie te implementeren.

WaitEU

Een realtime taak gebruikt hooguit één thread. Het idee is dat deze inactief is totdat de taak werk vereist, en dan zonder vertraging aan de slag gaat. In de praktijk betekent dat een maximale vertraging van 10 tot 20 ns. Voor sommige taken mag de vertraging echter best wat hoger zijn. In dat geval kunnen meerdere realtime taken in een enkele thread worden ondergebracht met behulp van events en/of interrupts.

Events en interrupts zijn twee subtiel verschillende mechanismen. Een interrupt is een welbekend hardwaremechanisme waarbij een externe taak aandacht kan vragen; als die een interrupt genereert, zet de processor alles opzij om naar de bijbehorende routine te springen. Deze moet de interrupt goed afhandelen: de interne toestand van de processor mag niet veranderd worden (sommige processoren bergen alle states op op de stack of in aparte schaduwregisters) en er moet voorzichtig worden omgesprongen met datastructuren waarmee het hoofdprogramma aan het werk is.

Een event werkt net als een interrupt, met één uitzondering: events worden alleen afgehandeld als het hoofdprogramma hierom vraagt. Omdat ze alleen in specifieke plekken van het hoofdprogramma worden afgehandeld, zijn events synchroon; een eventroutine weet dat het hoofdprogramma registers in een bruikbare toestand heeft achtergelaten en kan daarom meteen aan de slag en ook op datastructuren van het hoofdprogramma werken. Events zijn, net zoals interrupts, ingebouwd in de XCore-instructieset. Met een enkele instructie, WaitEU, kan een thread het volgende event afhandelen.

XCore voorbeeld
XMos uit Bristol ontwikkelt processorkernen waarin multithreading ingebakken zit: maximaal acht realtime threads kunnen op een enkele kern draaien. Daardoor is een realtime systeem met meerdere componenten eenvoudig te implementeren in een enkele XMos-processor.

Alle I/O-poorten, communicatie-einden en timers kunnen worden geprogrammeerd om ofwel een event ofwel een interrupt te veroorzaken als zich een specifieke conditie voordoet. Een timer kan bijvoorbeeld worden ingesteld om een interrupt te veroorzaken op het moment dat de klok 12311040 ns bereikt en een I/O-poort kan worden geconfigureerd om een event te veroorzaken zodra de code 1011 op de vier draden staat.

Dit geeft de flexibiliteit om, afhankelijk van de omstandigheden, op drie manieren te reageren op realtime gebeurtenissen: met een thread, door een WaitEU of via interrupts. De eerste methode geeft de kortste vertraging en is geschikt wanneer snelle reactietijden nodig zijn. Dit kost wel een thread. De tweede methode implementeert een toestandsmachine, waar iedere eventroutine de state achterlaat en dan met een WaitEU op de volgende statetransitie wacht. Deze methode heeft een grotere vertraging dan de eerste, maar kost geen thread. Interrupts zijn bruikbaar om fouten en andere weinig frequente gebeurtenissen op te vangen. Deze methode is het langzaamst, maar kan een thread op ieder moment interrumperen, ook als dat niet in het script stond. Interrupts kunnen de uitvoering van de code wel minder voorspelbaar maken.

Laagniveauprotocol

Het gebruik van meerdere threads om realtime taken op te lossen, vereist onderlinge communicatie en synchronisatie. Behalve gedeeld geheugen met locks kunnen threads daar channels voor gebruiken. Een channel heeft twee einden. Terwijl een thread aan het ene einde data schrijft, kan een tweede thread aan het andere einde gegevens uitlezen. Dat kan direct of door middel van interrupts of events. Net als met de scheduler zit channelcommunicatie in de XCore-instructieset ingebakken. Er zijn instructies om een eindpunt te verbinden met een ander eindpunt, om data over dit eindpunt te versturen, om data van dit eindpunt in te lezen en om controlegegevens te communiceren.

Het aardige van dit mechanisme is dat het direct naar multicore vertaalt. Dat is noodzakelijk als er te veel werk is voor een enkele core, bijvoorbeeld als er meer dan het maximum van acht hard realtime taken zijn of als er te veel geheugen, I/O of andere hardwarebronnen nodig zijn. Channels zijn dan een logische keus om communicatie tussen threads op verschillende cores te modelleren, omdat programmeur en compiler dezelfde instructies kunnen gebruiken voor zowel intra- als intercorecommunicatie. Het enige verschil is de latency: binnen een core kunnen threads met elkaar communiceren met een snelheid van 3,2 Gbit per seconde en een latency van 10 ns. Tussen de rekenkernen in een quadcoreconfiguratie bedraagt de latency tientallen nanoseconden. Over externe links neemt de latency toe tot een paar honderd nanoseconden en neemt de bandbreedte af tot maximaal 400 Mbit/s per link.

De XCore heeft een ingebouwd netwerk om twee of meer XCores via XLinks met elkaar te verbinden. Een XLink heeft vier of tien signaaldraden. Een ingebouwde (programmeerbare) switch kan data versturen van een thread aan de ene kant van het systeem naar een thread aan de andere kant. De ontwerper hoeft zich geen zorgen te maken over het laagniveauprotocol waarmee cores met elkaar communiceren. Het enige waar de ontwerper zich mee hoeft te bemoeien, is de bandbreedte en latency.

Heuristieken

Een van de belangrijkste redenen om meerdere threads, cores of microcontrollers te gebruiken, is om het systeem voorspelbare eigenschappen te geven. Dit gaat veelal verloren als een realtime systeem opgebouwd wordt rond een sequentiële processor, of soms een FPGA. De instructie- en datacaches van een gewone processor hebben de data soms beschikbaar, maar soms ook niet. Als twee taken parallel lopen, is het mogelijk dat ze elkaar in de cache in de weg zitten. Daarmee duurt parallelle executie (veel) langer dan verwacht. In een XCore heeft iedere instructie gegarandeerd toegang tot het geheugen.

In een FPGA zit het probleem aan het einde van de toolchain: totdat de routing- en placementtools van de FPGA aan het werk zijn geweest, is het onbekend hoe snel het ontwerp kan lopen. Een kleine lokale verandering in de code van de FPGA kan onvoorspelbare gevolgen hebben voor de snelheid van het hele systeem. In een XCore vertaalt dit probleem zich tot de communicatie en de plaatsing van de taakgraaf. De programmeur doet dit expliciet. Omdat er maar een tiental taken is, kost dit relatief weinig moeite. Op een FPGA, waar duizenden stukken logica moeten worden geplaatst, gebeurt dit automatisch met behulp van heuristieken die niet altijd hetzelfde antwoord geven.

ACH Xmos schema
Figuur 1: Een audio-over-Ethernet-systeem kan worden geïmplementeerd met een enkele XMos-processor waarop zeven threads draaien, waarvan vier hard realtime.

De ontwikkelcyclus voor de XMos-oplossing is min of meer hetzelfde als bij een verzameling microcontrollers. Het realtimeprobleem wordt beschreven als een C- of XC-programma, gecompileerd en uitgevoerd. XC is een versie van C waar concurrente programma‘s in kunnen worden beschreven met behulp van Par- en Select-statements. De C-compiler is een port van de LLVM-C-compiler; de XC-compiler hebben we zelf ontwikkeld om geoptimaliseerde multithreaded code te genereren.

De XC-programma‘s bevatten de controlflow (met conventionele if-then-else, while, for, et cetera), DSP-dataprocessing (multiply-accumulate), I/O-control (door een bitpatroon op een outputpin te zetten of een inputpatroon in te lezen op een inputpin) en integratie van alle activiteiten (waar processen selectief input, output of andere activiteiten uitvoeren). Omdat het systeem voorspelbaar is, kunnen onderdelen onafhankelijk worden ontwikkeld en in een laat stadium geïntegreerd.

Nibble

Als voorbeeld kunnen we een goedkoop audio-over-Ethernet-systeem ontwikkelen (zie Figuur 1). De noodzakelijke hardware bestaat uit een XCore en twee Phy‘s, voor Ethernet en de speaker. Deze hardware wordt bestuurd door zeven softwarethreads. Vier hiervan hebben hard-realtime-eisen: de Mac-ontvanger, de Mac-zender, de klokgenerator en de PWM-generator, het gedeelte dat de golf genereert. De drie threads die geen realtime-eisen hebben, moeten wel snel genoeg data afhandelen: de Mac-splitser, de frequentie-equalizer en de opsampler, die de frequentie opkrikt van 48 naar 480 kHz.

De Ethernet-Mac-ontvanger krijgt van de Phy ofwel een nibble (4 bits) data, ofwel een signaal dat het pakket volledig is ontvangen. De thread programmeert de inputpoort om op het bitpatroon ’1011‘ te wachten, het eind van de preamble. Daarna leest deze thread elke data-nibble in, bergt deze op en werkt vervolgens de checksum bij die voor de integriteitscontrole wordt gebruikt. De routine voor het eindepakketevent stuurt het pakket door naar de Mac-splitser. Die zendt vervolgens de digitale audio naar de equalizer en het klokprotocol (bijvoorbeeld IEEE 1588) naar de klokthread.

De klokthread krijgt de data van het timingprotocol (de precieze ontvangsttijd wordt geregistreerd door de Mac-ontvanger) en kan met behulp van een lokale timer een 480 kHz klok genereren voor de opsampler. De equalizer krijgt een stroom samples, die worden doorgestuurd naar de opsampler. Die stuurt de samples op de juiste tijd (aangegeven door de klokthread) door naar de PWM-generator. De PWM-thread ontvangt twee soorten events: een nieuw sample en een tik van de 100 MHz timer die aangeeft dat het PWM-signaal moet worden geïnverteerd.

Dit voorbeeld illustreert hoe threads kunnen worden gebruikt om meerdere soorten events af te handelen. De klok- en PWM-threads verwerken verschillende events tegelijk; de Mac-threads behandelen één event in een korte loop. De Mac-ontvanger gebruikt een extra event om uit de loop te springen aan het eind van het pakket.

Het gebruik van meerdere cores om realtime systemen te bouwen, is niet nieuw, maar met threads op een enkele fysieke core kunnen we het ontwerp vereenvoudigen doordat we minder componenten hoeven te integreren. Events geven de flexibiliteit om een thread meerdere realtime taken te laten uitvoeren.