== UCORE Programmierung == UCore (Utility Core) ist ein Eigenimplementierung eines 16 Bit Prozessor. Grundlegende Charakteristiken: • sechsstufige Pipeline • 8 Generalpurpose Registern • interne Speicher in dem Code/Daten liegen • externen Speicher Zugriff (32Bit-Adressraum) möglich • Die Datenwortbreite beträgt immer 16 Bit (es gibt keine Bytes) Weiterhin unterscheidet sich UCore einigen Dingen von einem 'normalen' Prozessor (RISC-)Design, die Ziele die hier verfolgt wurden waren eine möglichst hohe Taktrate und ein möglichst geringer Aufwand von Hardwareressourcen aber dennoch eine hohe Verarbeitungsgeschwindigkeit. Dadurch ergeben sich einige Besonderheiten die bei der Programmierung beachtet werden müssen (siehe Registerzugriffe, Branch/Jump Delayslots, Request/Load Zugriffe). === Pipeline === Wie gesagt hat dieser Core eine sechsstufige Pipeline: 1. Prepare Fetch 2. Fetch 3. Decode 4. Read Register 5. Execute 6. Write Register (hat einen Bypass zu Stufe 4) Jede Instruktion wird in einem Cycle ausgeführt, nur der externe Speicherzugriff (nur Daten) kann einen Stall (Stillstand) der Pipeline verursachen. == Interne Memory Map (Standardkonfiguration) == In meinem System wird die folgende interne Speichermap verwendet. $0000 code / data memory * 16384 * 16 bit * * $3fff $4000 code / data memory MIRROR * * * $7fff $8000 ctrl / memory StackPointer start at $8000 on reset * XXXX * 16 bit * * $XXXX Der Reset-Vektor befindet sich an Adresse 0, der Stackpointer an Adresse $8000. Der Interruptvektor an Adresse $10 (sollte nach dem Reset an die benötigte Position verschoben werden). Ab Adresse $8000 wird die Komponente UCtrl in den Adressraum ‚gemappt‘ (diese Komponente enthält Register für externe Hardware Peripherie etc.). == externe Memory Map (Standardkonfiguration) == Über den 'externen' Datenbus kann auf folgende Komponenten zugegriffen werden. $a000 0000 - $a000 001f SDCard-Controller $d000 0000 - $d3ff ffff SDRAM $e000 0000 - $e03f ffff FLASH $f000 0000 - $f00f ffff SRAM Bei den meisten meiner Programme wird ein Standard-Programm aus dem FLASH-Speicher gelesen, welches dann einen Speichertest ausführt und auf das Einlegen einer SD-Karte mit dem zuladenden Programm wartet. == Opcodes/Operanten == UCore hat keine 'RISC'-Typischen Befehlssatz. Da ein Instruktions-Wort nur 16 Bit breit ist, können nur bedingte Mengen an Operanden verwendet werden. Um jedoch die Programmierung flexibler zu gestalten, haben viele Opcodes mehrere Arten von Operanden. Es ergibt sich keine nachteilige Auswirkung auf die Ausführungseinheit, nur der Decoder ist etwas komplexer als bei anderen RISC Designs. Operanten Formate: dc,imm8 dc,db,da dc,db,imm3 imm12 db,da imm6 da kein Operant Opcode / Operante(n): movei dc,imm8 dc = imm moveih dc,imm8 dc[15..8] = imm << 8 | dc[7..0] addi dc,imm8 dc = dc + imm subi dc,imm8 dc = dc - imm lsri dc,imm8 dc = dc >> imm asri dc,imm8 dc = ((signed)dc) >> imm muli dc,imm8 dc = dc * imm cmpeqi dc,imm8 t = set if dc == imm else cleared cmploi dc,imm8 t = set if dc < imm else cleared cmplosi dc,imm8 t = set if (signed)dc < (signed) imm else cleared gpci dc,imm8 dc = pc + imm jmpi dc,imm8 pc = dc + imm extri dc,imm8 t = bit #imm of dc rqldi dc,imm8 request (internal) mem; address = dc + imm getsp dc,imm8 dc = sp + imm dexti imm11 store imm as extension for next instruction add dc,db,da dc = db + da sub dc,db,da dc = db - da lsr dc,db,da dc = db >> da asr dc,db,da dc = ((signed)db) >> da mul dc,db,da dc = db * da addt dc,db,da dc = db + da + t subt dc,db,da dc = db - da - t muls dc,db,da dc = db * da movets dc,db,da dc = db if t = set else da and dc,db,da dc = db & da or dc,db,da dc = db | da xor dc,db,da dc = db ^ da bic dc,db,da dc = db & ~da addqi dc,db,imm3 dc = db + imm subqi dc,db,imm3 dc = db - imm lsrqi dc,db,imm3 dc = db >> imm asrqi dc,db,imm3 dc = ((signed)db) >> imm mulqi dc,db,imm3 dc = db * imm3 addtqi dc,db,imm3 dc = db + imm + t subtqi dc,db,imm3 dc = db - imm - t edrqldi db,da,imm3 request (external) mem; address = (db:da) + imm3 br imm12 pc = pc + signed(imm) brts imm12 pc = pc + signed(imm) if t is set brtc imm12 pc = pc + signed(imm) if t is cleared cmpeq db,da t = set if db == da else cleared cmplo db,da t = set if db < da else cleared cmplos db,da t = set if (signed)db < (signed) da else cleared esadr db,da esadr = db:da swp db,da db = da[7..0]:da[15..8] swptc db,da db = da[7..0]:da[15..8] if t = set else da st db,da imem[db] = da stinc db,da imem[db] = da; db = db + 1 bffo db,da db is first bit that is 1 found in da (from MSB to LSB) bset db,da db = db | 1 << da bclr db,da db = db & ~ (1 << da) cmple db,da t = set if db <= da else cleared cmples db,da t = set if (signed)db <= (signed) da else cleared rqld db,da request (internal) mem; address = db + da extb db,da db = da byte to word singed extended stwo db,da imem[db + STORE_OFFSET] = da andts db,da db = db & da if t = set else da orts db,da db = db | da if t = set else da xorts db,da db = db ^ da if t = set else da bicts db,da db = db & ~da if t = set else da lsrts db,da db = db >> da if t = set else da asrts db,da db = ((signed)db) >> da if t = set else da mults db,da db = db * da if t = set else da mulsts db,da db = (signed)db * (signed)da if t = set else da erqldi imm6 request (external) mem; address = esadr + imm ssto imm6 STORE_OFFSET = imm udivinit imm6 udivqoutient = 0; udivrest = 0; udivbitcounter = imm eld da da = requested value from external memory push da sp = sp – 1; imem[sp] = da gmulhi da da = multiply result [31..15] from last multiplication ld/pop da da = requested value from internal memory poparqp da da = requested value from internal memory; request in internal memory adr = sp; sp = sp + 1 neg da da = 0 - da udivgq da get division qoutinent udivgr da get division remainder udivgqh da get division qoutinent high [31 ..16] udivgrh da get division remainder high [31 ..16] udivsdd da set division divident udivsdv da set division divider udivsddh da set division divident high [31 ..16] udivsdvh da set division divider high [31 ..16] erqld da request (external) mem; address = esadr + da setsp da sp = da erqldb da request (external) mem; address = esadr + da / 2 jmpts da pc = da if t = set jmptc da pc = da if t = cleared negts da da = 0 - da if t = set else da rqpop internal memory request address = sp; sp = sp + 1 cli disable interrupts sei enable interrupts rti pc = pc_before interrput tnt t = not t vtt t = v (overflow/underflow wrom add/sub) epushsadrl sp = sp – 1; imem[sp] = esadr[15..0] epushsadrh sp = sp – 1; imem[sp] = esadr[31..16] epopsadrl esadr[15..0] = requested value from internal memory epopsadrh esadr[31..16] = requested value from internal memory udivstep performe one division step nop do nothing === Registerzugriffe === Um ein ‚Registerscoring‘ und/oder extra Pipeline-Stalls zu vermeiden, muss man bei Registerabhängigkeiten (RAW = Read after Write) mindestens einen Cycle warten bzw. eine andere Operation ausführen. Beispiel (schreiben eines Registers und späteres lesen): movei r0,$ae ; nop ;write r0 = ae wird ausgeführt addi r0,1 ;r0 = r0 ($ae) + 1 Im einfachsten Fall, sollte eine nop Operation bei solchen Abhängigkeit verwendet werden. Beispiel (nicht optimiert mit nop - 6 cycles): movei r0,$ae nop add r0,r5 movei r1,$ef nop mul r1,r6 In den meisten Fällen kann man aber andere Operationen als ‚Füllung‘ bei Registerabhängikeiten verwenden. Beispiel (Optimiert - 4 cycles): movei r0,$ae movei r1,$ef add r0,r5 mul r1,r6 Um versehentlich solche ‚Hazards‘ zu erkennen erzeugt der Assembler Warnungen, ebenfalls kann der Emulator zur Laufzeit solche Probleme erkennen. Prinzipiell ist es möglich, in gewissen Situationen, Hazards absichtlich zuzulassen (Optimierungen um Register zu sparen), wovon aber im Normalfall abzuraten ist. === Vergleiche === UCore benutzt ein T-Bit (true Bit) um Bedingungen zu verarbeiten. Grundsätzlich wird dieses Bit von Vergleichsoperationen gesetzt und kann dann von Operationen benutzt werden welche das T-Bit intern verwenden (z.B. bedingte Sprünge). Beispiel: cmpeqi r0,0 ;vergleiche auf r0 gleich 0 wenn wahr da (t=1) ansonsten (t=0) brts r0IstNull ;springe zu ‚r0IstNull‘ label wenn t set (t=1) In vielen Fällen ist notwendig Register nur dann zu setzen wenn bestimmte Bedingungen eintreten. In diesen Fällen ist es ärgerlich bedingte Sprünge auszuführen, da sie etwas Zeit brauchen (Delay Slots) und den Programmcode unübersichtlich machen. Aus diesem Grund unterstützen einige Befehle eine Bedingte Ausführung (nicht jeder). Beispiel: move r1,123 move r2,23 … cmplo r0,r1 ;r0 < r1 movets r0,r2,r0 ;wenn ja r0 = r2 (23) ansonsten r0 = r0 === Sprünge === Ein (bedingter) Sprung benötigt, wie jede andere Instruktion, einen Cycle um ausgeführt zu werden. Da UCore keine Branchprediction bzw. keine spekulative Ausführung unterstützt und es auch nicht gewollt ist Opcodes in der Pipeline zu verwerfen, entstehen nach Sprungbefehlen sogenannte 'Delay Slots'. Die Anzahl dieser Delay Slots ergeben sich aus der Anzahl der Pipelinestufen vor der Ausführungseinheit (da sie den Programmcounter verändert). Kurzum Programlauftechnisch werden Instruktionen in diesen Delay Slots ausgeführt bevor der Sprung in Wirklichkeit ausgeführt wird. Die kleinste mögliche Schleife benötigt deshalb fünf Instruktionen für einen Durchlauf. Beispiel: loop br loop ;branch zu loop nop ;delay slot nop ;delay slot nop ;delay slot nop ;delay slot In vielen Fällen können diese Delay Slots sinnvoll genutzt werden. Beispiel 1 (nicht optimiert - 8 cycles): movei r0,1 ;r1 = x parameter movei r1,7 ;r2 = y parameter gpci r7,2 ;r7 = Rücksprungadresse für Unterfunktion br drawPixel nop ;delay slot nop ;delay slot nop ;delay slot nop ;delay slot Beispiel 1 (optimiert - 6 cycles): gpci r7,2 r7 = Rücksprungadresse für Unterfunktion br drawPixel movei r0,1 ;delay slot r1 = x parameter movei r1,7 ;delay slot r2 = y parameter nop ;delay slot nop ;delay slot Beispiel 2 (nicht optimiert - 8 * n + 3 cycles -> if n 128 -> total 1027 cycles) movei r0,127 ;r0 = loop counter - 1 movei r1,0 ;r1 = Füllwert nop loop st r1,r6 ;imem[r1] = r6 addi r1,1 ;r1++ subi r0,1 ;r0-- (t clear bei < 0) brts loop ;branch loop solange t set nop ;delay slot nop ;delay slot nop ;delay slot nop ;delay slot Beispiel 2 (optimiert - 6 * n + 2 cycles -> if n 128 -> total 770 cycles) movei r0,127 movei r1,0 loop subi r0,1 ;da t später verwendet wird nicht in delay slot benutzen (siehe Interruptbenutzung) brts loop st r1,r6 ;delay slot addi r1,1 ;delay slot nop ;delay slot nop ;delay slot === Speicherzugriffe === UCore besitzt keine ‚gewöhnlichen‘ Lesebefehle für interne/externe Speicherzugriffe. Um Pipeline-Stalls beim Lesen von Daten zu vermeiden (Latenz zum Speicher), wird eine Lesebefehl in zwei Operationen aufgeteilt. Der erste Befehl ist ein 'Request', er teilt dem Core mit von welcher Adresse er etwas lesen soll. Der zweite Befehl ist dann der 'Load', er holt sich die angekommenen Daten ab und speichert sie in einem Register. ==== interner Speicherzugriff ==== Bei internen Speicherzugriffen ist die Latenz zwischen Request und Load statisch (3 Cycles), bei externen Zugriff variable (hier wird im Fall eines Load ohne vorhandene Daten ein Stall ausgelöst). Mit den Opcodes rqld (request load) und ld (load) kann vom internen Speicher gelesen werden. Da die Daten erst zwei Operationen nach dem Request verfügbar sind, muss hier etwas anderes ausgeführt werde (z.B. nop). Beispiel: rqld r0,0 ;send load to address (r0+0) nop ;data send to memory nop ;data will read ld r1 ;readed data to r1 Da der interne Speicher immer innerhalb von einem Cycles antwortet und die Latenz durch Pipelinestufen entsteht, kann man bei internen Speicherzugriff bis zu drei Request hintereinander verwenden. Beispiel: rqld r0,0 rqld r0,1 rqld r0,2 ld r1 ;r1 <- internal_mem[r0+0] ld r2 ;r2 <- internal_mem[r0+1] ld r3 ;r3 <- internal_mem[r0+2] Bei Schreibzugriffen wird wie bei anderen Prozessoren ein Store-Kommando verwendet. Beispiel: dexti $80 ;imm exted move r4,$01 ;r4 = $8001 move r0,12 ;r0 = 12 ... st r0,r4 ;imem[r4] = r0; $8001 = 12 In vielen Fällen ist es leider etwas unangenehm mit der statischen Latenz bei internen Lesezugriffen, da man hier etwas mehr schreiben muss als man es von anderen Prozessoren gewöhnt ist. ==== externe Speicherzugriffe ==== Externe Speicherzugriffe verhalten sich ähnlich wie interne Zugriff. Unterschiede gibt es in der Adressbreite (extern 32Bit) und in der variablen Latenz (bestimmt durch mehre Faktoren). Es gibt eine externe Hauptadresse (Base) die mit 'allen' (bis auf edrqldi) externen Speicherzugriffsinstruktionen verwendet wird. Beispiel Hauptadresse setzen: esadr r1,r0 ;external address is r1:r0 Beispiel schreibe in externen Speicher: movei r4,$af ... esadr r1,r0 ;Hauptadresse (esadr) is r1:r0 est r4,0 ;externSpeicher[esadr + 0] = r4 Beim Lesen vom externen Speicher werden auch Request/Load Befehle verwendet um Latenzen, wenn möglich, zu vermeiden. Da die Latenz nicht vorhersehbar ist wird bei nicht vorhandenen Daten solange gewartet bis sie vorhanden sind (Stall). Beispiel: esadr r1,r0 ;external address is r1:r0 erqld 0 ;load from [r0:r1 + 0] eld r7 ;get external data (Stall solange bis Daten vorhanden) Um Lade/Schreiblatenzen zu vermeiden, können mehre Stores oder Request (laden) hintereinander ausgeführt werden (maximal 256 Requestzugriffe, keine Begrenzung bei Stores). Beispiel (schreibe 8 Worte in externen Speicher, kein Stall solange Schreibpuffer nicht voll): movei r0,0 esadr r7,r6 ;external address is r1:r0 est r0,0 est r0,1 est r0,2 est r0,3 est r0,4 est r0,5 est r0,6 est r0,7 Beispiel (vermeiden von Leselatenzen) esadr r7,r6 erqld 0 erqld 1 erqld 2 erqld 3 ;request 4 Worte .... ;mach irgendwas eld r0 eld r1 eld r2 eld r3 ;bekomme Daten (evtl. kein Stall, da Daten schnellgenug da) === Stack ==== Der Stackpointer zeigt nach dem Reset auf Adresse $8000 (also am oberen Ende des internen Speichers). Das Verhalten von push und request pops and pops sind gleich dem internen Speicherzugriffsverhalten (3 Cycles Latenz, Pipelined). Beispiel (1 pop): push r4 ;sp--; imem[sp] = r4 .... rqpop ;request imem[sp], sp++ nop nop pop r4 ;r4 = imem[sp] Beispiel (3 Werte auf Stack und wieder herunter) push r1 push r2 push r3 .... rqpop rqpop rqpop ;durch Latenz maximal 3 rqpop dann Daten (ansonsten 'poparqp' benutzen (kann Request und Load gleichzeitig)) push r3 push r2 push r1 === Decoder extemd == später === Interrupts === später === Divdieren === später === weiter Beispiele === später === UCtrl === später