16/32Bit Computer/Konsole
16/32 Bit Computer/Konsole
Einleitung
Dieses Projekt implementiert ein 'Computer'system, welches nicht auf irgendeinem vorhandenen Design (CPU etc.) beruht bzw. dieses nachbildet.
Die Idee ist/war ein System zu erstellen, welches vollständig* selbst geschrieben ist um in jeden Teil der Implementierung (Hardware/Software) flexibel zu sein und natürlich ein Verständnis für jede Komponente zu entwickeln. (*FIFO, PLLs, etc. wurden generiert).
Beschreibung
Als Hardwarebasis wird ein Altera DE2-115 Evaluations-Board benutzt. Zur Hardwarebeschreibung wird die Sprache VHDL verwendet, für Emulation und Tools (Assembler etc.) wird C# mit .NET benutzt.
Im Grundsystem wird ein Großteil der Peripherie des DE2-115 zur Verfügung gestellt:
(SRAM / SDRAM / FLASH / VGA / SDSLOT / HEX / LED / SW / LCD / AUDIO / EEPROM / PS/2)
Das Grundsystem implementiert einen 16 Bit Prozessor (UCORE = Utility Core), welcher über einen memorymapped UCTRL-Komponente vollständigen Zugriff auf alle Hardwarekomponenten hat.
Diese UCTRL-Komponente enthält:
- Programmierbares Video Interface (Auflösung/Scale/Farbformat/… programmierbar, SRAM/DRAM Port, H/V Hit/Counter etc.)
- Programmierbares Audio Interface
- Zwei I2C Ports (für WM8731 und EEPROM)
- Zwei PS/2 Ports
- Zwei Timer/Counter
- HCore Ctrl/Data Interface
- …
Der 16 Bit Core (UCORE) ist der zentrale Prozessor im System, d.h. er kann alle Hardwarekomponenten ansprechen (indirekt (UCTRL), direkt (SRAM/DRAM/…)) und ist dazu gedacht, eine einfache Steuerungen durch eine Softwareimplementierung zu ermöglichen. Nichtsdestotrotz ist seine Verarbeitungsgeschwindigkeit recht hoch (ca. 106MIPS) und ich habe ihn, für alle meine Demos/Spiele, als Hauptprozessor benutzt. In Zukunft sollen aber softwarelastige Aufgaben in den HCore Block verlagert werden (32 Bit Cores).
Die Charakteristiken von UCORE sind:
- Interner Code / Daten Speicher
- 6 Stufige Pipeline (eine Cycle pro Instruktion)
- 16 Bit Ausführungseinheit (Arithmetisch, Logisch, Bitfeld, … Operationen)
- 32 Bit externer Port zu SRAM/DRAM/FLASH/SDCard/…
- 8 GP-Register
- Spezielle request / load Befehle um Latenzen beim externen Speicherzugriff zu vermeiden
- Erweiterungen für Grafik Beschleunigungen
In einen experimentellen Zustand befindet sich noch der HCORE Block. Dieser Block enthält mehre (konfigurierbar) 32 Bit Prozessoren die über spezielle Techniken (Hardwaresemaphoren, parallele Sprünge, … ) eine parallele Verarbeitung von Software vereinfachen sollen und Latenzprobleme bei Multithreading vermeiden.
Software
Um die Softwareentwicklung (Debuggen etc.) zu vereinfachen, existiert einen Emulator des Systems.
Der Emulator deckt alle Hardwarekomponenten des Systems ab, bietet aber noch viele Möglichkeiten der Überwachung (runtime Disassembler, Registerstatus, Hardwaremonitor, …) an.
Für UCORE und HCORE gibt es einen Assembler der den Maschinencode erzeugt (in Emulator integriert). Ein Compiler (z.B.: für C) existiert bis jetzt nicht.
Downloads
- Dokumentation, System Source Code (Emulator/Tools/VHDL/), Assembler Dateien für alle Demos/Spiele, …
Demos
Sheriff 2213 (Level 1-5) http://www.youtube.com/embed/WcUinTuIObA
SpacePilotOfDeath FINAL http://www.youtube.com/embed/ctVRjXSUPnw
MOD Player final Version http://www.youtube.com/embed/W0ChHagGEAQ
test GFX instructions http://www.youtube.com/embed/ke_z5vKetXk
Tilemapper test http://www.youtube.com/embed/uKNQfi4BDl8
Emulator overview http://www.youtube.com/embed/MKoS0bCZ_38
MOD Player beta 2 Version http://www.youtube.com/embed/Yy9mM5lHSA0
MOD Player beta Version http://www.youtube.com/embed/O9RUoHj54Gg
SRAM to SDRAM software rotozoom http://www.youtube.com/embed/ZE8hn5Nqa4g
MOD Player alpha Version http://www.youtube.com/embed/I83u6VxUcw0
6 Channel Audio Test http://www.youtube.com/embed/aH5pF4WBsVU
Space Pilot of Death updated Version http://www.youtube.com/embed/eK75rcHrX0c
self written MODPlayer for Windows http://www.youtube.com/embed/Y04lMvfCgm8
Boot from SDCard http://www.youtube.com/embed/mlu24aY84MY
Playing video from SDCard (uncompressed) http://www.youtube.com/embed/uCjt68XQOQ4
UCore drawing some 'software' sprites into framebuffer http://www.youtube.com/embed/_GDSuAsApBg
Space Pilot of Death running on goldmomo_endlos http://www.youtube.com/embed/q64eGbGddhk
UCore make motion blur on framebuffer (inject some 'fire' objects) http://www.youtube.com/embed/hZ0Bh3xF3x4
UCore memory test http://www.youtube.com/embed/Pr89wex3YbU
UCore PS/2 KEyboard interface test http://www.youtube.com/embed/eNDLkwafubE
Emulator of goldmomo_endlos running native code http://www.youtube.com/embed/xZNF1klLHL0
UCORE Programmierung
UCORE unterscheidet sich einigen Teilen von einem normalen Prozessordesign.
Die Gründe bzw. Ziele welche verfolgt wurden, waren eine möglichst hohe Taktrate und ein möglichst geringe Benutzung von Hardwareressourcen, aber dennoch eine hohe Verarbeitungsgeschwindigkeit.
Dadurch ergeben sich einige Besonderheiten, welche 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 (adressiere Daten)
- 2 Fetch (hole Daten)
- 3 Decode (dekodiere Daten)
- 4 Read Register (lese Register)
- 5 Execute (ausführen)
- 6 Write Register (schreibe Register (hat einen Bypass zu Stufe 4, somit keine extra Delay)
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 (16 Bit interner Adressraum).
$0000 code / data memory * 16384 * 16 bit * * $3fff $4000 code / data memory MIRROR * * * $7fff $8000 uctrl / memory StackPointer start at $8000 on reset * XXXX * 16 bit * * $XXXX
Der Reset-Vektor befindet sich an Adresse $0000, der Stackpointer an Adresse $8000 (am Ende des internen RAMs).
Der Interruptvektor zeigt nach dem Reset auf Adresse $0010 (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 (32 Bit Adressraum) 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 Boot-Programm aus dem FLASH-Speicher gelesen, welches einen Speichertest ausführt und auf das Einlegen einer SD-Karte, mit dem zu ladenden Programm, wartet.
Opcodes/Operanden
UCore hat keine 'RISC'-Typischen Befehlssatz (benutzt viele Befehle, aber mit geringe Komplexität).
Da ein Instruktions-Wort nur eine Breite von 16 Bit hat, können nur bedingte Mengen an Operanden verwendet werden. Um jedoch die Programmierung flexibler zu gestalten, haben viele Opcodes mehrere Arten von Operanden. Für die Ausführungseinheit (ALU) ergibt sich dadurch keine Ressourcenveränderung, da der Decoder nur die Opcodes/Operanden in ein internes einheitliches Format auflöst.
Operanden können bis zu drei Register verwenden und je nach Typ einen festen Wert, dieser Wert ist je nach Typ in seiner Bitbreite beschränkt (kann mittels Decoderinstruktion dexti erweitert werden).
Operanden Formate
dc,imm8 Register, 8 Bit Wert (unsigned) dc,db,da Register, Register , Register dc,db,imm3 Register, Register, 3 Bit Wert (unsigned) imm12 12 Bit Wert (signed) db,da Register, Register imm6 6 Bit wert (unsigned) da Register kein Operant
Die Opcodes selber können in folgenden (klassischen) Typen eingeordnet werden.
- Transfer-Befehle (move, ld, st, rqld ,...)
- Arithmetische Befehle (add, sub, mul, neg,…)
- Logische Befehle (and, or, xor, bic,…)
- Bit-Befehle (bset,bclr,bffo,..)
- Vergleichs-Befehle (cmpeq, cmplo,…)
- Schiebe-Befehle (lsr,asr, swp,…)
- Programmsteuerungs-Befehle (br, jmp, rti…)
- Status-Befehle (sei,cli, getssr, …)
Opcode / Operand(en) / Beschreibung
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
Da Register vor der Ausführungseinheit (ALU) gelesen werden und nach der Ausführungseinheit geschrieben werden, ergibt sich eine Latenz zwischen Register Zugriffen. Der Grund für diese Implementierungsart ist, schlicht und einfach, die Performanz zu erhöhen (weniger Logik pro Takt). Als Nebeneffekt entsteht eine Latenz zwischen den Zugriffen. Es bieten sich mehrere Möglichkeiten an, diese Register-'Hazards' in Hardware zu vermeiden (Registerscoring, Renaming etc.). Um den Hardwareimplementierung klein und schnell zu halten, wird in UCore auf einen Hardwaretest verzichtet. Als Resultat, muss der Anwender Registerabhängigkeiten im Programm-Code behandeln/beachten. Der einzige Nachteil (der durch eine Latenz entsteht) ergibt sich in einer RAW (Read after Write) Abhängigkeit (da die Latenz hier eine Cycle beträgt). In diesem Fall sollte/muss eine andere Instruktion (z.B. nop) eingefügt werden.
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 aber eine andere Operationen als 'Füllung' bei Registerabhängigkeiten verwendet werden.
Beispiel (optimiert - 4 cycles):
movei r0,$ae movei r1,$ef add r0,r5 mul r1,r6
Um versehentlich programmierte RAW-'Hazards' zu erkennen, erzeugt der Assembler Warnungen, ebenfalls kann der Emulator diese zur Laufzeit erkennen.
Prinzipiell ist es möglich, in gewissen Situationen, RAW-'Hazards' absichtlich zuzulassen (z.B. Optimierungen um Register zu sparen), wovon aber im Normalfall abzuraten ist.
Arithmetische Befehle
Arithmetische Befehle unterscheiden sich, in UCore, nicht von anderen Prozessoren–Designs. Neben Addition, Subtraktion, Multiplikation existiert noch eine Instruktion zum Negieren. Division wird über mehre Instruktion unterstützt (siehe Division). Zu beachten ist, dass bei Addition, Subtraktionen Carry/Borrow immer ins t-Flag übernommen wird und das Overflow-Flag in den Statusregister gespeichert wird.
Arithmetische Befehle wie addt,subt,… benutzen das t-Flag als Carry/Borrow-In. Durch diesen Mechanismus können Operationen mit mehr als 16 Bit-Breite (32 Bit etc.) einfach implementiert werden.
Beispiel (32 Bit Addition)
add r0,r0,r2 addt r1,r1,r3 ;r1:r0 = r1:r0 + r3:r2
Die Multiplikationsoperationen haben als Ergebnis ein 32 Bit wert. Die unteren 16 Bit dieses Wertes geben die Multiplikationen selbst zurück. Die oberen 16 Bit werden intern gespeichert und können mit der Instruktion gmulhi in einen Register transferiert werden. Logische Befehle:
Neben den üblichen logischen Befehlen wie and, or, xor, ist die Instruktion bic (bit clear = and not) implementiert. Alle logischen Befehle können mit zusätzlicher Bedingung ausgeführt werden.
Beispiel (bedingter logischer Befehl)
cmplosi r0,0 ;t = true wenn r0 < 0 xorts r0,r0 ;wenn t = true r0 = r0 xor r0 (r0 = 0)
Bit Befehle
Da die Breite vom Immediate-Werte, je nach Instruktions-Typ, begrenzt ist, sind Bitoperationen mit logischen Befehlen meist umständlich, in Software, zu implementieren. Zum Setzen bzw. Löschen sind die Instruktionen bset, bclr implementiert worden. Die Instruktion bffo (bit find first one) sucht von MSB zu LSB, das erste Bit mit Wert Eins.
Die Instruktion extri (extract immediat), transferiert das angegeben Bit in das t-Flag.
Beispiel (extri)
extri r0,15 ;bit 15 in r0 -> t-Flag (ist Wert negative) negts r0 ;wenn t = true dann r0=0-r0
Schiebeoperationen
UCore implementiert nur Schiebeoperation in die „rechte“ – Richtung (Arithmetisch rechts schieben (asr), Logisch rechts schieben (lsr)). Um Werte nach links zu schieben, sollte die Multiplikationoperation verwendet werden. Um Bytes in einem Register zu tauschen sollte die Operation swp (swap) verwendet werden.
Vergleichsoperationen
Vergleichsoperation setzen in UCore immer als Ergebnis das t-Flag (siehe Bedingungen), d.h. jede Art von Vergleich hat ihren eigenen Befehl (keine Statusflags wie z.B. bei 68k).
Neben dem Vergleich auf den gleichen Wert (cmpeq = compare equal), gibt es Vergleiche auf ‚<=‘ (cmple[s] (unsigned und signed)) und auf ‚<‘ (cmplo[s] ] (unsigned und signed)). Vergleiche auf ‚>‘‚ ‘>=‘ bzw. nicht gleich sind nicht vorhanden, da sie logisch durch den Wert des t-Flags, bei der Benutzung der gegenteiligen Operationen, bestimmt werden können.
Bedingungen
UCore benutzt ein T-Bit (true/wahr Bit) um Bedingungen zu beschreiben/verarbeiten. Normalerweise (nicht ausschließlich) wird dieses Bit von Vergleichsoperationen gesetzt und kann nachfolgend von Operationen benutzt werden welche das T-Bit abfragen (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 zum 'r0IstNull' Label wenn t set (t=1)
In vielen Fällen ist notwendig Register nur dann zu setzen, wenn bestimmte Bedingungen eintreten. In solchen Fällen ist es ärgerlich bedingte Sprünge auszuführen, da sie etwas Zeit benötigen (siehe Delay Slots) und den Programmcode unübersichtlich machen (Spaghetticode).
Aus diesem Grund, unterstützen einige Befehle eine bedingte Ausführung.
Beispiel:
move r1,123 move r2,23 … cmplo r0,r1 ;r0 < r1 movets r0,r2,r0 ;wenn wahr/true 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 etc. unterstützt und es auch nicht gewollt ist Opcodes in der Pipeline zu verwerfen (wie z.B. bei MC68020), 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 (bzw. 'gefetched' werden). Die kleinst 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 '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 Interruptverarbeitung) 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 Opcode 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 eine andere Instruktion 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, können 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 st (Store-Kommando) verwendet.
Beispiel:
dexti $80 ;imm extend move r4,$01 ;r4 = $8001 move r0,12 ;r0 = 12 ... st r0,r4 ;imem[r4] = r0; $8001 = 12
Da ein store Befehl nur einen Register als Zieladresse benutzt, ist es vielen Fällen ärgerlich, diesen bei mehrfachen Schreiben zu verändern. Um in diesem Fall Abhilfe zu schaffen, wurde der Befehl ssto (Set Store Offset) und stwo (Store with Offset) implementiert. Mit ssto kann ein Offset für stwo gesetzt werden, d.h. der Wert der mit ssto gesetzt wird auf die Zieladresse von stwo addiert.
Beispiel:
stwo 1 ssto r0,r7 ;imem[r7+1] = r0 stwo 2 ssto r1,r7 ;imem[r7+2] = r1
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 32 Bit) 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 ;Hauptadresse ist 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 ebenfalls Request/Load Befehle verwendet um Latenzen, wenn möglich, zu vermeiden. Da die Latenz (meist) nicht vorhersehbar ist, wird bei nicht vorhandenen Daten solange gewartet bis sie vorhanden sind (Stall).
Beispiel:
esadr r1,r0 ;Hauptadresse (esadr) ist r1:r0 erqld 0 ;lade von externSpeicher[r0:r1 + 0] eld r7 ;lade externe Daten (evtl. Stall solange bis Daten vorhanden)
Um Lade/Schreiblatenzen zu vermeiden bzw. zu reduzieren, können mehre Stores oder Request 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 ;Hauptadresse (easdr) ist r7:r6 est r0,0 ;externSpeicher[esadr + 0] = r0 est r0,1 ;externSpeicher[esadr + 1] = r0 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 ;Hauptadresse (easdr) ist r7:r6 erqld 0 ;request 4 von Worten request externSpeicher[easdr+0] erqld 1 ; request externSpeicher[easdr+1] erqld 2 ;... erqld 3 .... ;mach irgendwas (Daten werden im 'Hintergrund' geladen) eld r0 ;Daten von erstem request ( externSpeicher[easdr+0] ) eld r1 ;Daten von zweiten request ( externSpeicher[easdr+1] ) eld r2 ;... eld r3 ;
Stack
Der Stackpointer zeigt nach dem Reset auf Adresse $8000 (also am oberen Ende des internen Speichers). Das Verhalten von push, request pops und 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 extend
Da viele Operationen konstante Werte (in Operand kodiert) benutzen können, aber diese aus Gründen der Befehlsbreite (16 Bit) je nach Befehl/Operanden nur eine teilweise unzureichende Größe/Breite verwenden können, wurde eine Dekoder Instruktion dexti (decoder extend immediate) eingeführt.
Dieser Befehl wird vom Dekoder ausgeführt und wird in der eigentlichen Ausführungseinheit ignoriert (nop).
Sinn dieses Befehls ist es, konstante Werte um einen 8 Bit breiten Wert zu erweitern. Um Fehler zu vermeiden ist dieser Wert nur einen Cycle nach dexti verfügbar.
Beispiel (16 Bit immediate Addition)
dexti $12 addi r0,$34 ;r0 = r0 + $1234
Interrupts
Interrupts sind in einem Pipeline basierenden Design immer ärgerlich (IMHO) zu handhaben. Da UCore nicht, wie viele Mikrokontroller, möglichst kurze Interrupt-Latenzen ermöglichen muss, gibt es in diesem Design auch keine garantierten Zeiten in welchen der Interrupt ausgeführt wird.
Um den Hardwareaufwand so gering wie möglich zu halten, habe ich eine sogenannte Branch-Injektion implementiert. Der Hintergedanke ist, dass Sprünge sehr oft im Code ausgeführt werden und diese, im Fall eines Interrupts, dazu missbraucht werden können, direkt zum Interrupt-Service-Vector zu springen, anstatt ihren eigentlichen definierten Sprung auszuführen (dieser wird gespeichert und später ausgeführt).
Der Vorteil dieser Implementierung ist ein sehr geringer Hardwareaufwand, der Nachteil ist, dass die Interrupt-Latenz variiert (Delta zwischen zwei Sprüngen) und sich Verhaltensregeln für 'Delay-Slots' ergeben.
Beispiel:
sei ;irq enable br anySubroutine ;if irq is enabled and occure a branch is taken to irq vector and after irq is finished (rte) the give branch will taken nop ;delay slot nop ;delay slot nop ;delay slot nop ;delay slot
In 'Delay-Slots' sollte kein T-Bit gesetzt werden, welches für spätere Verwendungen gebraucht wird.
Außerdem sollten keine Speicher-Request (ohne Loads) an diesen Stellen ausgeführt werden.
Um Fehler zu vermeiden warnt der Assembler bei solchen Konstrukten.
Beispiel:
sei br anySubroutine addt r0,r1 ;delay slot (t is not save in delay slot if irq is enabled, because t is only store at br instruction) nop ;delay slot nop ;delay slot nop ;delay slot
Dividieren
Viele Prozessoren die auf geringen 'Platzverbrauch' optimiert sind, versuchen wenn möglich Divisions-Operationen zu vermeiden oder führen diesen nicht in einer Pipeline-Implementierung aus.
Auf der Softwareseite sind Divisionen in vielen Fällen vermeidbar oder durch bestimmte Methoden ersetzbar. Prinzipiell ist eine Division einfach in Software zu implementieren, in diesem Fall ist die Geschwindigkeit einer Hardwareimplementierung meist nicht annähernd zu erreichen.
UCore bietet spezielle Befehle an, um einen Kompromiss aus Geschwindigkeit und Hardwareaufwand zu ermöglichen.
Wie bei einer normalen Division wird der Divisor und Dividend gesetzt, zusätzlich muss noch die Divsions-Bitbreite angegeben werden (max. 32 Bit). Durch den Befehl udivstep wird eine '1-Bit Division' ausgeführt, d.h. udivstep muss/kann sooft ausgeführt werden wie die Divisionsbreite gewählt ist. Als Resultat der Division ergeben sich der Quotient und der Rest.
Beispiel:
;****************************** ;* ;* udiv16 ;* ;* r0 divident ;* r1 divisor ;* ;* return ;* ;* r2 qoutient ;* r3 remainder ;* ;* needs 34 cycles ;* udiv16 udivinit 15 udivsdd r0 udivsdv r1 movei r0,3 ;(3+1)*4 = 16 cli ;disable irq udiv16Loop subi r0,1 brts udiv16Loop udivstep ;delay slot !!!! note udivstep is not irq save, so use cli/sei lock udivstep ;delay slot udivstep ;delay slot udivstep ;delay slot jmpi r7,0 nop ;delay slot udivgq r2 ;delay slot get qoutient udivgr r3 ;delay slot get remainder sei ;delay slot enable irqs
weiter Beispiele
später
UCtrl
UCtrl ist eine Abstraktionsschicht für einen Großteil der externen (Altera-DE2-115) Peripherien und stellt zusätzlich hilfreiche Element (Timer,…) zur Verfügung.
Direkten Zugriff auf diese Komponente hat nur UCore (siehe Memorymap).
UCtrl bietet dem Benutzer zugriff auf:
- LEDS
- SWITCHES
- 7-Segment Anzeige
- LCD (zweizeilig auf DE2-115)
- Interrupt Kontrolle (für UCore)
- Timer/Counter
- Video Setup (inkl. Counter und Vertikal-‚Hit‘-Erkennung)
- PS/2 Port (in/out, zwei Ports)
- I2C (zwei Ports)
- Audio (DAC/ADC)
- HCore Setup und Daten (beschreibe ich später)
Register
----------------------------------------------------------- address = $8000 name = LEDG_SW read fedcba9876543210 SSSSSSSSSSSSSSSS S = switches on DE2-115 write fedcba9876543210 -------GGGGGGGGG G = LED Green ('1' is ON) ----------------------------------------------------------- address = $8001 name = LEDR read fedcba9876543210 --------------ME M = '1' if running in modelsim E = '1' if running in emulator write fedcba9876543210 -------RRRRRRRRR R = LED Red ('1' is ON) ----------------------------------------------------------- address = $8002 - $8005 name = 7SEG read fedcba9876543210 2 ---------------- 3 ---------------- 4 ---------------- 5 ---------------- write fedcba9876543210 2 -6543210-6543210 3 -6543210-6543210 4 -6543210-6543210 5 -6543210-6543210 -6543210-6543210 = Hex Segment of Hex 1 / 0 ('1' is ON) -6543210-6543210 = Hex Segment of Hex 3 / 2 ('1' is ON) -6543210-6543210 = Hex Segment of Hex 4 / 4 ('1' is ON) -6543210-6543210 = Hex Segment of Hex 5 / 6 ('1' is ON) ----------------------------------------------------------- address = $8006 - $8007 name = LCD read fedcba9876543210 6 -------BDDDDDDDD 7 ---------------- B = '1' if read/write request is busy D = Data after request note: wait 1 cycle after read/write request before read write fedcba9876543210 6 ------WRDDDDDDDD 7 ---------------- W = '0' is request write, '1' is request read R = LCS RS D = data to write ----------------------------------------------------------- address = $8008 - $8009 name = IRQ read fedcba9876543210 8 -----------HADTT IRQ MSK 9 -----------HADTT IRQ MEMORY TT = Timer 2/1 IRQ D = Audio DAC FIFO run low A = Audio ADC FIFO run full H = HCORE has data write fedcba9876543210 8 -----------HADTT IRQ MSK 9 -----------HADTT IRQ MEMORY TT = Timer 2/1 IRQ D = Audio DAC FIFO run low A = Audio ADC FIFO run full H = HCORE has data description: IRQ MSK is used to enable given interrupts (for example D = '1' then Audio DAC irq will be used) IRQ MEMORY reflect after interrupt which interrupt is the source (for example TT = '01' then Timer 1 made this interrupt) You should/have to clear the given interrupt bit in IRQ MEMORY before leaving interrupt service routine. ----------------------------------------------------------- address = $800A - $800F name = TIMERS read fedcba9876543210 A DDDDDDDDDDDDDDDD TIMER_1_LOW (Timer 1 value low) B DDDDDDDDDDDDDDDD TIMER_1_HIGH (Timer 1 value high, note: low/high needs capture first to be updated) C ---------------- D DDDDDDDDDDDDDDDD TIMER_2_LOW (Timer 2 value low) E DDDDDDDDDDDDDDDD TIMER_2_HIGH (Timer 2 value high, note: low/high needs capture first to be updated) F ---------------- write fedcba9876543210 A DDDDDDDDDDDDDDDD TIMER_1_LOW (Timer 1 value low) B DDDDDDDDDDDDDDDD TIMER_1_HIGH (Timer 1 value high, write to high will update) C ------------CLSR TIMER_1_CTRL D DDDDDDDDDDDDDDDD TIMER_2_LOW (Timer 1 value low) E DDDDDDDDDDDDDDDD TIMER_2_HIGH (Timer 1 value high, write to high will update) F ------------CLSR TIMER_2_CTRL C = Capture counter value for read (copy current counter value to $A/B $D/E) L = Enable Auto Reload Value (after counter value runs to 0 next value is $A/B $D/E) S = Stop timer R = Start timer (will do a Autoreload from $A/B $D/E) ----------------------------------------------------------- address = $8010 - $802F name = VIDEO read fedcba9876543210 10 00000DDDDDDDDDDD current HCount (11..0) 11 000000DDDDDDDDDD current VCount (10..0) 12 ---------------H Hit of programmed VCOUNT 13 ---------------- 14 ---------------- 15 ---------------- 16 ---------------- 17 ---------------- 18 ---------------- 19 ---------------- 1a ---------------- 1b ---------------- 1c ---------------- 1d ---------------- 1e ---------------- 1f ---------------- 20 ---------------- 21 ---------------- 22 ---------------- 23 ---------------- 24 ---------------- 25 ---------------- 26 ---------------- 27 ---------------- 28 ---------------- 29 ---------------- 2a ---------------- 2b ---------------- 2c ---------------- 2d ---------------- 2e ---------------- 2f ---------------- write fedcba9876543210 10 ---------------D VIDEO ON if '1' 11 ------DDDDDDDDDD HSYNC 12 ------DDDDDDDDDD HSTART 13 ------DDDDDDDDDD HMEMSTART 14 ------DDDDDDDDDD HSTOP 15 ------DDDDDDDDDD HTOTAL 16 -------DDDDDDDDD VSYNC 17 -------DDDDDDDDD VSTART 18 -------DDDDDDDDD VSTOP 19 -------DDDDDDDDD VTOTAL 1a DDDDDDDDDDDDDDDD LINE CACHE ADDRESS ADD LOW 1b DDDDDDDDDDDDDDDD LINE CACHE ADDRESS ADD HIGH 1c ---------------- LINE CACHE LATCH VALUE 1d -------DDDDDDDDD LINE CACHE START ADDRESS (Pixel offset) 1e --------DDDDDDDD HITVCOUNT (see description) 1f ---------------- 20 ------DDDDDDDDDD HLOADSTART 21 ------DDDDDDDDDD HLOADSTOP 22 -------DDDDDDDDD VLOADSTART 23 -------DDDDDDDDD VLOADSTOP 24 -------DDDDDDDDD VLOADNEXT 25 DDDDDDDDDDDDDDDD EXTERNAL MEMORY START ADDRESS (low) 26 DDDDDDDDDDDDDDDD EXTERNAL MEMORY START ADDRESS (high) 27 DDDDDDDDDDDDDDDD EXTERNAL MEMORY LINE OFFSET (low) 28 DDDDDDDDDDDDDDDD EXTERNAL MEMORY LINE OFFSET (high) 29 --------------DD MEMORY Convertion MODE 2a ---------------- LATCH MEMORY CONFIG 2b ---------------- 2c ---------------- 2d ---------------- 2e ---------------- 2f ---------------- HITVCOUNT = Programmable VCOUNT if this Value is written $12 bit 0 is set to '1' if VCOUNT is equal to this value $12 bit 0 is cleared at write to this register EXTERNAL MEMORY LINE OFFSET = will be add after every loaded line (for non interleaved video setup) MEMORY Convertion MODE = defines the format of data converted from external memory (ever 32Bit fetch) to line cache 00 = X8 R8 G8 B8 (32 Bit = (8 Bit for Red/Gree/Blue)) 01 = R5 R6 R5 R5 R6 R5 (2 * 16 Bit = (5 Bit for Red/Blue and 6 Bit for Green)) 10 = X1 R5 R5 X1 R5 R5 R5 R5 (2 * 16 Bit = (5 Bit for Red/Gree/Blue)) 11 = X4 R4 R4 R4 X4 R4 R4 R4 (2 * 16 Bit = (4 Bit for Red/Gree/Blue)) ----------------------------------------------------------- address = $8030 - $8031 name = PS/2 port a/b read fedcba9876543210 30 -----FWVDDDDDDDD PS2_PORT_A (port A) 31 -----FWVDDDDDDDD PS2_PORT_B (port B) D = PS/2 DATA (Scan Code) valid if A = '1' V = Data valid W = Write FIFO not full F = all writes finished (Fifo with 16 entries) note: wait 1 cycle after read/write request before read write fedcba9876543210 30 R-----WADDDDDDDD PS2_PORT_A (port A) 31 R-----WADDDDDDDD PS2_PORT_B (port B) D = PS/2 DATA to write A = get next scan code (read 2 cycles after this from $30/$31) W = Write data to PS/2 device R = reset PS/2 state machine (in/out) ----------------------------------------------------------- address = $8032 - $8033 name = I2C port a/b read fedcba9876543210 30 ------------D--- I2C_PORT_A (port A (eeprom on DE2-115)) 31 ------------D--- I2C_PORT_B (port B (audio/video in on DE2-115)) D = data in from port A/B write fedcba9876543210 30 ----EEE------ICD I2C_PORT_A 31 ----EEE------ICD I2C_PORT_B D = data out value C = clock out value I = direction of data ('0' = in / '1' = out) EEE = is write enable for ICD ----------------------------------------------------------- address = $8034 - $803b name = AUDIO read fedcba9876543210 34 DDDDDDDDDDDDDDDD ADC_DATA_LEFT_HIGH 35 --------DDDDDDDD ADC_DATA_LEFT_LOW 36 DDDDDDDDDDDDDDDD ADC_DATA_RIGHT_HIGH 37 --------DDDDDDDD ADC_DATA_RIGHT_LOW 38 ---------------- 39 ---------------- 3a ---------------- 3b ---------------- write fedcba9876543210 fedcba9876543210 34 DDDDDDDDDDDDDDDD DAC_DATA_LEFT_HIGH 35 --------DDDDDDDD DAC_DATA_LEFT_LOW 36 DDDDDDDDDDDDDDDD DAC_DATA_RIGHT_HIGH 37 --------DDDDDDDD DAC_DATA_RIGHT_LOW 38 AAAAAAAADDDDDDDD ADAC_CTRL (Data) 39 --------DDDDDDDD DAC_IRQ_GEN (interrupt generate if lower) 3a --------DDDDDDDD ADC_IRQ_GEN (interrupt generate if higher) 3b ---------------- ADC_NEXT (recive values (after 2 cycles available in ADC_DATA ($34-$37))) ADAC_CTRL (Data) = configure internal ADC/DAC controller Adr 76543210 0 -------- ignore 1 -------E enable DAC 2 ----CCCC DAC push counter (96KHz at 0, 48KHz at 1, 32KHz at 2 ....) 3 -------E enable ADC 4 ----CCCC ADC pull counter (96KHz at 0, 48KHz at 1, 32KHz at 2 ....) ----------------------------------------------------------- address = $803c - $8044 name = HCORE CTRL/IO (in experimental state)
Videosetup im Detail
UCore und UCtrl (also auch der Videokontoller) haben eine Taktfrequenz von 106,481 MHz, mit dieser Frequenz ist es möglich eine maximale Auflösung von 1440 x 900 Bildpunkten bei 60 Hz zu programmieren.
Da diese Auflösung in vielen Fällen zu hoch ist, bzw. die Performance für eine flüssige Bildberechnung nicht immer ausreicht, kann ein interner Zeilen-Cache so programmiert werden, dass er nur eine bestimmte Anzahl an Pixeln pro Zeile ausgibt (H-Skaliert) und nur eine definierte Anzahl von Zeilen neu geladen wird (V-Skaliert).
Wie üblich können (siehe Bild), HSYNC, HSTART etc. frei programmiert werden.
Als Quelle für die Bilddaten kann der SDRAM oder SRAM benutzt werden (siehe EXTERNAL_MEMORY_START_ADDRESS).
Das Farbformat kann zwischen einem 32 Bit Format (8Bit für R/G/B) und drei 16 Bit Formaten gewählt werden (siehe MEMORY Convertion MODE).
Ein Support von Bitplanes oder Tiled-Grafiken gibt es nicht.
Beispiel Video setup 360x225 4X4R4G4B Mode ($d0000000 ist Quelle (SDRAM)):
;****************************** ;* ;* setupVideo ;* setupVideo movei r0,videoDefault ;source movei r1,UcTimer2Ctrl ;dest-1 moveih r0,>videoDefault moveih r1,>UcTimer2Ctrl movei r2,26 ;27-1 setupVideoLoop rqldi r0,0 addi r0,1 addi r1,1 ld r3 subi r2,1 brts setupVideoLoop st r1,r3 ;delay slot nop ;delay slot nop ;delay slot nop ;delay slot jmpi r7,0 nop ;delay slot nop ;delay slot nop ;delay slot nop ;delay slot ;********************************************************************* ;* data ;********************************************************************* videoDefault ;AD NAME word $0001 ;10 VIDEO_ON word $0097 ;11 HSYNC (D = 11 .. 0) word $017f ;12 HSTART (D = 11 .. 0) word $017d ;13 HMEMSTART (D = 11 .. 0) word $071f ;14 HSTOP (D = 11 .. 0) word $076f ;15 HTOTAL (D = 11 .. 0) word $0002 ;16 VSYNC (D = 10 .. 0) word $001e ;17 VSTART (D = 10 .. 0) word $03a2 ;18 VSTOP (D = 10 .. 0) word $03a3 ;19 VTOTAL (D = 10 .. 0) word $0000 ;1a LC ADDER LOW word $0040 ;1b LC ADDER HIGH word $0000 ;1c LC ADDER LATCH word $0000 ;1d LC START (10 .. 0) Pixeloffset word $0000 ;1e word $0000 ;1f word $0008 ;20 HLOADSTART word $00bc ;21 HLOADSTOP word $001e ;22 VLOADSTART word $03a2 ;23 VLOADSTOP word $0004 ;24 VLOADNEXT word $0000 ;25 MEM_STARTADR_store low word $d000 ;26 MEM_STARTADR_store high word $0000 ;27 MEM_LINEOFFSET_store low word $0000 ;28 MEM_LINEOFFSET_store high word $0002 ;29 MEM_MODE_store word $0000 ;2a LATCH MEM_STARTADR/LINEOFFSET/MODE
Audiosetup im Detail
Da das DE-115 einen Wolfson WM8731 besitzt, unterteilt sich die Ansteuerung für die Audio-Ein/Ausgabe in zwei Teile.
Die Erste, ist die Konfiguration des WM8731, hierfür wird I2C Port B von UCTRL verwendet.
Um die Konfiguration einfach zu halten sind Komponenten zur Initialisierung über I2C vorhanden (initAudio), es muss nur eine Tabelle mit den gewünschten Einstellungen übergeben werden (Register, Wert siehe WM8731 Registerbeschreibung).
Beispiel Setup:
audioDefault ; ;A = 8 7654 3210 .data $9,$0000 ;9 = 0 0000 0000 (inactivate interface) .data $0,$0097 ;0 = 0 1001 0111 ;left line in mute .data $1,$0097 ;1 = 0 1001 0111 ;right line in mute .data $2,$006e ;2 = 0 0111 1001 ;+6db left out .data $3,$006e ;3 = 0 0111 1001 ;0db right out .data $4,$0012 ;4 = 0 0001 0010 ;0=0 mic boost off; ;1=1 enable line input mute ;2=1 line input to adc ;3=0 disable bypass ;4=1 DAC select ;5=0 side tone disable ;76=0 -6db sidetone .data $5,$0000 ;5 = 0 0000 0000 ;DAC soft Mute off, no Filer .data $6,$0067 ;6 = 0 0110 0111 ;DAC Power, Output Power, Device Power on .data $7,$0009 ;7 = 0 0000 1001 ;left justified, 24 bit .data $8,$001e ;8 = 0 0001 1110 ;0=0 normal mode ;1=1 bosr (384/192) ;5432=0111 96khz ;78=00 no clk dividing .data $9,$0001 ;9 = 0 0000 0001 (activate interface) .data $ff,$0000 ;stop
Der zweite Teil, das Senden und Empfangen der Audio Daten wird über UCTRL Register $34 bis $3b ermöglicht(ADC/DAC_DATA, ADAC_CTRL, ADC/DAC_IRQ_GEN, ADC_NEXT).
Für ADC und DAC gibt es jeweils für den linken & rechten Kanal je 24 Bit Daten zu empfangen/senden.
ADC/DAC besitzen je einen 256 Worte (2 * 24 Bit) großen Queue, im Register DAC_IRQ_GEN ($39) bzw. ADC_IRQ_GEN ($3a) können die die Unter/Überlaufwerte, welche zu einem Interrupt führen, konfiguriert werden.
D.h. für einen Audioausgabe, konfiguriert man z.B. den DAC Queue auf den Wert 128 um beim Erreichen (kleiner als) einen Interrupt auszulösen (in welchen der Queue befüllt wird).
Dadurch ist eine unterbrechungsfreie Wiedergabe/Aufnahme möglich ohne (je nach Einstellung) zu viele Interrupts auszulösen.
Die ADC/DAC Komponente in UCTRL kann über Register ADAC_CTRL ($38) konfiguriert werden.
Die oberen 8 Bit des Datenworts bestimmen die Adresse, die unteren 8 Bit die Daten.
UCTRL Audio Config:
name address 876543210 comment nop 0 XXXXXXXXX do nothing ctrl 1 XXXXXXXXE E = enable audio (default off) set fifo read count 2 XXXXXXCCC 96KHz/C = fread [0..15] set fifo write count 3 XXXXXXCCC 96KHz/C = fwrite [0..15]
Hier sollte vor der eigentlich Audio Ausgabe/Aufnahme, der jeweilige Counter für den ADC/DAC gesetzt werden (Counter der bei erreichen von 0 liest/schreibt). Z.B. den Wert 2 um eine 32 KHz Ausgabe einzustellen. Um die Queues zu lesen/schreiben muss noch unter Adresse 1 die Komponente aktiviert werden.
Beispiel Audioausgabe von zwei 16 Bit Werten (links/rechts).
;r2 Audiobase $34 ;r1 = Wert für L, r4 = Wert für R movei r3,0 stinc r2,r1 ;dac left high = value movei r7,$1 stinc r2,r3 ;dac left low = 0 moveih r7,$1 stinc r2,r4 ;dac right high = value nop stinc r2,r3 ;dac right low = 0 nop st r2,r7 ;dac address/ctrl = $0101 ;ctrl = enable audio
Audio Tools
Um eine einfache Handhabung der Audioausgabe zu ermöglichen, gibt es unter dem Verzeichnis assembler_files\ucore\components Funktionen, die eine einfache Audioausgabe (Sample) und das Abspielen von MOD (Musik-Module) ermöglichen.
PS/2 im Detail
Mit den Registern PS2_PORT_A/B kann auf die PS/2 Schnittstelle des DE2-115 zugegriffen werden. Um alle Scancodes zu verarbeiten, ist ein Eingangsqueue (von PS/2) mit 16 Einträgen und einen Ausgangsqueue (zu PS/2) mit 8 Einträgen implementiert worden
Zum Schreiben eines 8 Bit breiten Wertes zum PS/2-Device kann/sollte vor dem eigentlichen Transfer geprüft werden, ob der Ausgangsqueue voll ist (Bit 9 = W in PS2_PORT_A/B).
Der zu sendende Datenwert muss in den unteren 8 Bit des Registers (PS2_PORT_A/B) geschrieben werden. Mit Bit 9 (W = write) wird er dann schließlich in den Ausgangsqueue übertragen.
Mit Bit 10 (F = writes finish) kann, je nach Bedarf, überprüft werden ob alle Daten übermittelt wurden.
Da es unter Umständen beim Schreiben zu Fehlern kommen kann (PS/2 Device defekt oder nicht angeschlossen bzw. es generiert keinen Takt), ist es möglich mittels Bit 15 R die State-Maschine zurückzusetzen.
Beim Lesen eine Wertes von dem PS/2-Device, muss mittels Bit 8 (V = data valid (lesen)) geprüft werden, ob Daten im Eingangsqueue vorhanden sind, wenn dies zutrifft, wird mit setzen von Bit 8 (A = ACK (schreiben)) das Datenwort in die unteren 8 Bit des Register übertragen (zwei Cycles delay).
Um die Handhabung mit einer PS/2 Tastatur zu vereinfachen, sind unter components/ps2keyboards.s Funktionen implementiert, welche die Steuerung und Auswertung (zu Ascii etc.) übernehmen.
Beispiel: testen auf Taste 'Escape':
;process keys (read from PS/2 Device and process) gpci r7,2 ;lr br PS2Process nop ;delay slot nop ;delay slot nop ;delay slot nop ;delay slot ;key test (ESC) gpci r7,2 ;lr br PS2TestKey movei r0,118 ;test again 'ESC' delay slot nop ;delay slot nop ;delay slot nop ;delay slot ; cmpeqi r0,0 ;r0 = 0 no key pressed brtc escIsPressed ;jump . . .
I2C Ports im Detail
Auf dem Altera DE2-115 befinden sich mehrere Komponenten (Audio-Codec, Video-In, EEPROM) die mittels des I2C-Protokolls konfiguriert werden.
Um diese Komponenten ansteuern zu können, besitzt UCtrl zwei Ports welche die jeweiligen Pins (SDA/SCK) der Komponenten bedienen können. Das eigentliche I2C-Protokoll ist in Software umgesetzt, d.h. mit den I2C_PORT_A/B Registern können nur Pins gesetzt bzw. ihr Zustand gelesen werden.
Zum Daten lesen (SDA) wird Bit 3 (D) von Register I2C_PORT_A/B benutzt.
Zum Konfigurieren des Daten-Pins (Datenrichtung) wird Bit 2 (I) verwendet. Hierbei ist zu beachten, dass beim Schreiben immer das zugehörige Enable gesetzt werden muss (Bit 11-9). Also beim setzen des CLK-Pins (z.B.: $22 = Enable CLK, setze CLK = '1').
Wie schon erwähnt, ist das eigentliche I2C Protokoll in Software umgesetzt. Unter components/i2c.s sind die benötigten Funktionen implementiert.
Timer im Detail
UCtrl besitzt zwei integrierte 32 Bit große Timer/Counter.
Konfiguriert werden kann jeder Timer über Register TIMER_1/2_CTRL.
Um den Startwert bzw. den aktuellen Wert des Timers/Counters zu lesen (bzw. zu schreiben) werden die Register TIMER_1/2_LOW/HIGH benutzt.
Beim Schreiben ist zu beachten, dass TIMER_1/2_HIGH nach TIMER_1/2_LOW geschrieben werden muss.
Beim Lesen muss das Bit 3 (C = Capture Counter) einmalig gesetzt werden um den aktuellen Wert in die Register TIMER_1/2_HIGH/LOW (Lesezugriff) zu laden.
Gestartet und gestoppt wird jeder Timer mit Bit 1/0 (S = Stop, R = Start) im Register TIMER_1/2_CTRL.
Der Timer übernimmt beim Start den 32 Bit großen/breiten Wert aus Register TIMER_1/2_LOW/HIGH und dekrementiert (-1) diesen solange bis er den Wert 0 erreicht.
Wenn der Wert 0 erreicht ist, wird ein "Timer-Hit" ausgelöst. Dieser kann je nach Bedarf einen Interrupt auslösen (siehe Register IRQ_MSK/MEMORY).
Um einen kontinuierlichen Lauf zu ermöglichen, kann Bit 2 (L = Auto Reload) aktiviert werden, in diesem Fall wird bei dem Erreichen von Wert 0, der Wert aus TIMER_1/2_LOW/HIGH geladen und der Timer startet erneut.
Beispiel vom Setzen und Starten des Timers 2:
dexti >UcTimer2LowValue movei r2,UcTimer2LowValue ;timer 2 base move r0,240 ;low value move r1,0 ;high value stinc r2,r0 ;low movei r0,5 ;start with auto reload stinc r2,r1 ;high nop stinc r2,r0 ;write config (lets go)
SD-Zugriff im Detail
Über die Implementierung der SD-Karten Schnittstelle möchte ich hier nicht im Detail eingehen (es sei denn, es wird gewünscht).
Zum benutzen der SD-Karte sind unter components/sdCardLoader.s Funktionen vorhanden.
Die Software (Block-IO) unterstützt SD-SC/HC/XC Karten, als Ziel für die zu lesenden Daten kann der SRAM/DRAM benutzt werden. Die maximale Transfergeschwindigkeit beträgt 100Mbit.
Beispiel, nachladen von 16384 Blöcken (a 256 Bytes):
;read sdcard to s/sdram dexti >startBlock movei r0,startBlock nop rqldi r0,0 ;low rqldi r0,1 ;high nop ld r2 ld r3 addi r2,64 ;64 blocks offset (ucore code/data) addtqi r3,r3,0 ;set destination address movei r0,$10 movei r1,$02 moveih r0,$b1 moveih r1,$d0 ;r1:r0 = $d002 b110 (dest start) dexti >sdCardReadBlocks movei r6,sdCardReadBlocks gpci r7,2 ;lr jmpi r6,0 movei r4,$ff ;16384 blocks-1 delay slot nop ;delay slot moveih r4,$2f ;delay slot nop ;delay slot
Emulator
Um den Test von Software zu vereinfachen, habe ich einen Emulator entwickelt, welcher das System auf einen ‚Windows‘ – System (als Host) emuliert. Der Emulator emuliert, in vielen Bereichen, Zyklen genau.
d.h. das Verhalten zwischen der Hardwareimplementierung und den Emulator ist identisch (nicht die Geschwindigkeit des Systems).
Um ein Sammelsurium von Tools beim Entwickelt zu vermeiden, sind mehre Tools im Emulator integriert (können aber auch einzeln ausgeführt werden):
- Assembler (UCore, HCore)
- Grafikkonverter
- SD-Karten RAW IO (lesen/schreiben)
Um den Status der Hardware zu überprüfen, wurden Hardware-Views implementiert:
- UCore (Disassembler, Register, Status, Pipeline, …)
- HCore (Disassembler,…)
- Video Registerübersicht
- Audio Registerübersicht
- SD-Karten IO-Übersicht
- Speicheransicht (IRAM,SRAM,SDRAM, …)
Die Geschwindigkeit der Emulation ist ungefähr 10-20 mal langsamer als auf der eigentlich Hardware. Um das Debuggen /Testen zu beschleunigen, kann die Software erkennen (wenn gewünscht) ob sie in einer Emulation läuft und so diverse Wartefunktionnen überspringen (z.B. warten auf VBlank).
Starten des Emulators
Unter dem Verzeichnis cross_development\binaries die Dateien ucore_emulator.exe ausführen. Unter Windows 8 kann es vorkommen, dass diese Datei als von einem unbekannten Hersteller angezeigt wird und das Ausführen extra bestätigt werden muss. Administratorrechte etc. werden nicht benötigt.
Laden eines Packages
Packages sind generierte Archive in denen sich alle benötigten Daten (IRAM, Flash, SDData,…) für einen Testlauf befinden. Sie sind mit wenigen Aufwand ladbar und vereinfachen die Handbarkeit des Emulators (wenn kein Debuggen erwünscht ist).
Um ein Packages zu laden, muss unter dem Reiter io im Bereich package das gewollte Package ausgewählt werden ‚…. (Alle generierten Packages sind unter dem Verzeichnis packages verfügbar).
Danach mittels des Buttons Load kann das Package geladen werden. Wenn das erfolgt ist, kann das Programm mittels Start (Play-Knopf in der Toolleiste) gestartet werden.
Forum
http://www.mikrocontroller.net/topic/310121
Homepage