16/32Bit Computer/Konsole

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

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:

System Überblick

(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

Emulator

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

Demos

Sheriff 2213
SpacePilotOfDeath
MOD Player
GFX Extension

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. 1 Prepare Fetch (adressiere Daten)
  2. 2 Fetch (hole Daten)
  3. 3 Decode (dekodiere Daten)
  4. 4 Read Register (lese Register)
  5. 5 Execute (ausführen)
  6. 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

Video Setup

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

audio simple

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

Ucore emulator4 info.png
Ucore emulator3 info.png

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).

Ucore emulator2 info.png

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.

Ucore emulator1 info.png

Forum

http://www.mikrocontroller.net/topic/310121

Homepage

http://www.goldmomo.de