Zeichenfunktionen: Unterschied zwischen den Versionen
Die Seite wurde neu angelegt: „Zeichenfunktionen“ |
KKeine Bearbeitungszusammenfassung |
||
| Zeile 1: | Zeile 1: | ||
Zeichenfunktionen | In diesem Kapitel werden wir einige Zeichenfunktionen erstellen. Am Ende können wir Linie, Rechtecke, Kreise und Zeichen anzeigen lassen und unser Ziel wird der hier dargestellte Bildschirm sein. | ||
[[Datei:Zeichenfunktionen.png|rand|800x800px]] | |||
Um diesen Bildschirm anzeigen zu lassen, könnt ihr den folgenden Code [https://www.satyria.de/arm/source/kurs/6.1.zip 6.1.zip] verwenden. In den folgenden Beispielen, wird nur auf die jeweilige Funktion eingegangen, die hier verwendet wurden. Die Funktion "DrawColor" wird gesondert behandelt. | |||
Bisher können wir einen Punkt in einer bestimmten Farbe anzeigen. Da aber das Leben nicht nur aus Punkten besteht, werden wir ein paar Grafik Routinen erstellen, damit es einfacher wird. Als erstes werden wir eine Linie auf den Bildschirm zeichen. | |||
== DrawLine (Linie) == | |||
Vom Grunde her denkt man, dass das zeichnen von Linien relativ einfach wäre, aber das ist weit verfehlt. Überlege dir mal, wie du sowas angehen könntest. Die erste Überlegen, auf die man eingehen muss, ist das wir nur ein Koordinatensystem zu verfügen haben. Solange wir nur 90° Winkel benutzen scheint es relativ einfach zu sein. Aber sobald eine Linie einen anderen Winkel verwendet, wird es schwieriger. | |||
Bei der nächsten Überlegung kommt man auf Formeln, die Multiplikationen oder Divisionen verwendet. Damit könnte man bestimmte Steigungen berechnen. Multiplikationen könnte man noch verwenden, wenn hier der Zeitfaktor keine Rolle spielt. Divisionen werden aber von der CPU ohne Co-Prozessor nicht unterstützt. | |||
Aus diesem Grund müssen wir hier einen anderen Weg finden. | |||
Nach Recherche im Internet (warum alles neu erfinden) habe ich den "Bresenhams Algorithmus" gefunden, wie er auch auf der Seite “[https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ Baking Pi – Operating Systems Development]“ im Kapitel “Lesson 7 Screen02” verwendet wird. | |||
Also verwenden wir diese und schreiben diese für unser System um. | |||
Zunächst sichern wir die Position von Anfang und Ende der Linie. | |||
<syntaxhighlight lang="asm"> | |||
DrawLine: | |||
push {r4,r5,r6,r7,r8,r9,r10,r11,r12,lr} | |||
mov r9,r0 @r9 = x1 | |||
mov r10,r2 @r10 = x2 | |||
mov r11,r1 @r11 = y1 | |||
mov r12,r3 @r12 = y2 | |||
</syntaxhighlight> | |||
Wir müssen zunächst DeltaX und das StepX berechnen. Da es von der Reihenfolge, größe des Wertes der Reihenfolge abhängig ist, vergleichen wir beide x-Positionen und reagieren entsprechend. | |||
<syntaxhighlight lang="asm"> | |||
cmp r9,r10 @Compare x1 with x2 | |||
@If x1 is greater than x2 | |||
subgt r4,r9,r10 @DeltaX = x1 - x2 | |||
movgt r6,#-1 @StepX = -1 | |||
@Else (x1 less than / equal to x2) | |||
suble r4,r10,r9 @DeltaX = x2 - x1 | |||
movle r6,#1 @StepX = 1 | |||
</syntaxhighlight> | |||
Das gleiche machen wir für die Y-Position. | |||
<syntaxhighlight lang="asm"> | |||
cmp r11,r12 @Compare y1 with y2 | |||
@If y1 greater than x2 | |||
subgt r5,r12,r11 @DeltaY = y1 - y2 | |||
movgt r7,#-1 @StepY = -1 | |||
@Else (y1 less than / equal to y2) | |||
suble r5,r11,r12 @DeltaY = y2 - y1 | |||
movle r7,#1 @StepY = 1 | |||
</syntaxhighlight> | |||
Als nächstes setzen wir die Ausgangswerte. | |||
<syntaxhighlight lang="asm"> | |||
add r8,r4,r5 @Error = DeltaX + DeltaY | |||
add r10,r6 @x2 = x2 + StepX | |||
add r12,r7 @y2 = y2 + StepY | |||
</syntaxhighlight> | |||
Nun können wir die Linie zeichnen. Innerhalb der Schleife, die wir durchführen, prüfen wir, ob x1 und x2 oder y1 und y2 identisch sind. Dies würde die Schleife beenden und gleichzeitig auch die Funktion. | |||
<syntaxhighlight lang="asm"> | |||
DrawLineLoop$: | |||
teq r9,r10 @Compare x1 with x2 | |||
teqne r11,r12 @if unequal; Compare y1 with y2 | |||
popeq {r4,r5,r6,r7,r8,r9,r10,r11,r12,pc} @if equal then end | |||
</syntaxhighlight> | |||
Als nächstes laden wir die aktuelle Position und zeichnen einen Punkt. | |||
<syntaxhighlight lang="asm"> | |||
mov r0,r9 @copy x1 to r0 | |||
mov r1,r11 @copy y1 to r1 | |||
bl DrawPixel | |||
</syntaxhighlight> | |||
Über DeltaY und “Fehler x 2” wird festgestellt ob x1 verändert wird, sowie für DeltaX und “Fehler x 2” für y1. Danach kann die Schleife wiederholt werden. | |||
<syntaxhighlight lang="asm"> | |||
cmp r5, r8,lsl #1 @If DeltaY is less or equal than (error * 2) | |||
addle r8,r5 @error = error + DeltaY | |||
addle r9,r6 @x1 = x1 + StepX | |||
cmp r4, r8,lsl #1 @If DeltaX greater than (error * 2) | |||
addge r8,r4 @error = error + DeltaX | |||
addge r11,r7 @y1 = y1 + StepY | |||
b DrawLineLoop$ @calculate next point | |||
</syntaxhighlight> | |||
Wie sieht es nun im Hauptprogramm aus? | |||
Im Hauptprogramm müssen wir zunächst alle Includes laden, die wir verwenden, definieren unseren Screen und starten unser Programm. | |||
<syntaxhighlight lang="asm"> | |||
.include "base.inc" | |||
.include "screen.h" | |||
.include "drawing.h" | |||
.equ SCREEN_X, 1920 | |||
.equ SCREEN_Y, 1080 | |||
.equ BITS_PER_PIXEL, 32 | |||
.section .init | |||
.globl _start | |||
_start: | |||
b main | |||
.section .text | |||
main: | |||
mov sp,#0x8000 | |||
</syntaxhighlight> | |||
Den Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawLine.zip DrawLine.zip] | |||
=== Neue Befehle (1) === | |||
==== subgt, movgt ==== | |||
sub und mov sind bereits bekannt. Allerdings wurde ihnen der Bedingungscode für größer mit Vorzeichen übergeben. Dies bedeutet, dass diese nur durchgeführt werden, wenn zuvor das Ergebnis entsprechend ausfiel. | |||
==== suble, movle, addle ==== | |||
Auch diese Befehle hatten wir bereits. Das Suffix “le” bedeutet, das der Befehl nur Ausgeführt wird, wenn das Ergebnis kleiner oder gleich mit Vorzeichen war. | |||
==== teq, teqne ==== | |||
Dieser Befehl testet zwei Werte und aktualisiert die Bedingsflags. Allerdings wird das Ergebnis nicht in ein Register geschrieben, wie es CMP macht. | |||
Der Suffix "ne" bedeutet, dass der Befehl nur ausgeführt wird, wenn das zuvor festgestellte Ergebnis nicht gleich war. | |||
==== popeq ==== | |||
Setzt die Daten aus dem Stack zurück in die angegebenen Register, wenn zuvor das Ergebniss “gleich” war. | |||
==== addge ==== | |||
Befehl wird ausgeführt, wenn das Ergebnis zuvor Größer oder gleich (mit Vorzeichen) war. | |||
== DrawBox (Rechteck) == | |||
Nachdem wir nun Linie zeichnen können, ist es relativ einfach, eine Rechteck zu zeichnen. Wir verbinden einfach die Ecken miteinander, und wir haben unser Rechteck. | |||
<syntaxhighlight lang="asm"> | |||
DrawBox: | |||
/* | |||
void DrawBox (x0, y0, x1, y1) | |||
*/ | |||
push {r4,r5,r6,r7,lr} | |||
mov r4,r0 @r4 = x0 | |||
mov r5,r1 @r5 = y0 | |||
mov r6,r2 @r6 = x1 | |||
mov r7,r3 @r7 = y1 | |||
mov r0,r4 | |||
mov r1,r5 | |||
mov r2,r6 | |||
mov r3,r5 | |||
bl DrawLine | |||
mov r0,r6 | |||
mov r1,r5 | |||
mov r2,r6 | |||
mov r3,r7 | |||
bl DrawLine | |||
mov r0,r6 | |||
mov r1,r7 | |||
mov r2,r4 | |||
mov r3,r7 | |||
bl DrawLine | |||
mov r0,r4 | |||
mov r1,r7 | |||
mov r2,r4 | |||
mov r3,r5 | |||
bl DrawLine | |||
pop {r4,r5,r6,r7,pc} | |||
</syntaxhighlight> | |||
Den Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawBox.zip DrawBox.zip] | |||
== DrawFillBox (Gefülltes Rechteck) == | |||
Wenn wir nun ein gefülltes Rechteck erstellen wollen, so zeichnen wir einfach ein Linie neben der anderen. Wir verwenden dafür einfach "y0" und erhöhen y0 um 1 und vergleichen es mit y1. Wenn wir diesen Wert erreicht haben, sind wir fertig. | |||
<syntaxhighlight lang="asm"> | |||
DrawFillBox: | |||
/* | |||
void DrawFillBox (x0, y0, x1, y1) | |||
*/ | |||
push {r4,r5,r6,r7,lr} | |||
mov r4,r0 @r4 = x0 | |||
mov r5,r1 @r5 = y0 | |||
mov r6,r2 @r6 = x1 | |||
mov r7,r3 @r7 = y1 | |||
DrawFillBoxLoop: | |||
mov r0,r4 | |||
mov r1,r5 | |||
mov r2,r6 | |||
mov r3,r5 | |||
bl DrawLine | |||
add r5,#1 | |||
cmp r5,r7 | |||
ble DrawFillBoxLoop | |||
pop {r4,r5,r6,r7,pc} | |||
</syntaxhighlight> | |||
Den Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawFillBox.zip DrawFillBox.zip] | |||
== DrawCircle (Kreis) == | |||
Wenn man sich in der Mathematik gut auskennt, kennt man die Funktionen für SINUS und COSINUS. Mit beiden Funktionen wäre es relativ einfach, einen Kreis zu erzeugen. Leider haben wir zu diesem Zeitpunkt keine Cosinus oder Sinusfunktion. Zusätzlich sollten wir, wie bei den Linien auch, auf Divisionen verzichten. | |||
Wenn man etwas im Internet forscht, findet man den Bresenham’s Kreis Algorithmus. Diesen werden wir hier nun verwenden. Mehr informationen zu diesem Algorithnus findet man zum Beispiel unter https://de.wikipedia.org/wiki/Bresenham-Algorithmus. | |||
Folgenden Pseudo-C-Code verwenden wir für unsere Routine: | |||
<syntaxhighlight lang="c"> | |||
DrawCircle: | |||
/* | |||
void DrawCircle(int x0, int y0, int radius) | |||
*/ | |||
int x = radius; | |||
int y = 0; | |||
int err = 0; | |||
while (x >= y) | |||
{ | |||
putpixel(x0 + x, y0 + y); | |||
putpixel(x0 + y, y0 + x); | |||
putpixel(x0 - y, y0 + x); | |||
putpixel(x0 - x, y0 + y); | |||
putpixel(x0 - x, y0 - y); | |||
putpixel(x0 - y, y0 - x); | |||
putpixel(x0 + y, y0 - x); | |||
putpixel(x0 + x, y0 - y); | |||
y += 1; | |||
err += 1 + 2*y; | |||
if (2*(err-x) + 1 > 0) | |||
{ | |||
x -= 1; | |||
err += 1 - 2*x; | |||
} | |||
} | |||
</syntaxhighlight> | |||
Also Wandeln wir einfach diesen Code in Assembler um. | |||
Zunächst sichern wir wieder unsere Übergabewerte. | |||
<syntaxhighlight lang="asm"> | |||
DrawCircle: | |||
/* | |||
void DrawCircle(int x0, int y0, int radius) | |||
*/ | |||
push {r4,r5,r6,r7,r8,r9,r10,r11,lr} | |||
mov r4,r0 @r4 = x0 | |||
mov r5,r1 @r5 = y0 | |||
mov r6,r2 @r6 = radius | |||
</syntaxhighlight> | |||
Und setzen die Ausgangswerte für unsere Funktion. | |||
<syntaxhighlight lang="asm"> | |||
@ int x = radius; | |||
mov r7,r6 @r7 = X | |||
@ int y = 0; | |||
mov r8,#0 @r8 = y | |||
@ int err = 0; | |||
mov r9,#0 @r9 = err | |||
</syntaxhighlight> | |||
Als nächstes wird im C-Code eine While-Schleife definiert. Dies bedeutet, dass die Schleife solange durchgeführt wird, wie das Ergebnis wahr ist. Sobald das Ergebnis falsch ist, wird die Schleife abgebrochen. Damit erstellen wir nun in unserem Code auch eine Schleife. | |||
<syntaxhighlight lang="asm"> | |||
DrawCircleLoop: | |||
@ while (x >= y) | |||
@ { | |||
... | |||
... | |||
@ } | |||
cmp r7,r8 | |||
bcs DrawCircleLoop | |||
</syntaxhighlight> | |||
Für die entsprechende "putpixel" Funktion verwenden wir unsere "DrawPixel"-Funktion. Allerdings werden dir Koordinaten mittels einer Berechnung der Funktion übergeben. Diese Berechnung müssen wir zuvor durchführen und geben dann das Ergebnis unserer "DrawPixel"-Funktion. | |||
<syntaxhighlight lang="asm"> | |||
@ putpixel(x0 + x, y0 + y); | |||
add r0,r4,r7 | |||
add r1,r5,r8 | |||
bl DrawPixel | |||
@ putpixel(x0 + y, y0 + x); | |||
add r0,r4,r8 | |||
add r1,r5,r7 | |||
bl DrawPixel | |||
... | |||
... | |||
</syntaxhighlight> | |||
Nun setzen wir folgende Berechnung in Assembler um: | |||
<syntaxhighlight lang="c"> | |||
y += 1; | |||
err += 1 + 2*y; | |||
if (2*(err-x) + 1 > 0) | |||
{ | |||
x -= 1; | |||
err += 1 - 2*x; | |||
} | |||
</syntaxhighlight> | |||
<code>y += 1;</code> Dieser Code in "C" bedeutet das gleiche wie <code>y = y + 1</code>. Also y wird einfach um eins erhöht. Umgesetzt, da wir "r8" als y definiert haben, addieren wir einfach 1 mit "r8". | |||
<syntaxhighlight lang="asm"> | |||
@ y += 1; | |||
add r8,#1 | |||
</syntaxhighlight> | |||
Die nächste Zeile <code>err += 1 + 2*y;</code> nehmen wir mal auseinander. | |||
Zunächst haben wir die Berechnung <code>2*y</code>. Eine Multiplikation um 2 können wir mit einem "lsl" durchführen, wie wir bereits zuvor gelernt haben. Das Zwischenergebnis legen wir in r10 ab. | |||
<syntaxhighlight lang="asm"> | |||
mov r10,r8,lsl #1 | |||
</syntaxhighlight> | |||
Bei der nächsten Berechnung wird einfach nur eins addiert. Dies dürfte für uns auch kein Problem sein: | |||
<syntaxhighlight lang="asm"> | |||
add r10,#1 | |||
</syntaxhighlight> | |||
<code>err +=</code> bedeutet ja wieder, dass wir das Ergebnis hinzuaddieren müssen. Für <code>err</code> verwenden wir das Register r9. | |||
<syntaxhighlight lang="asm"> | |||
add r9,r9,r10 | |||
</syntaxhighlight> | |||
Komplett entspricht es diesen Code: | |||
<syntaxhighlight lang="asm"> | |||
@ err += 1 + 2*y; | |||
mov r10,r8,lsl #1 | |||
add r10,#1 | |||
add r9,r9,r10 @ r9 = err | |||
</syntaxhighlight> | |||
Die nächste Anweisung, die wir umsetzen müssen, ist eine IF-Anweisung. IFs können wir mit bedingten Branch durchführen. Allerdings müssen wir zuvor die entsprechende Abfrage, hier die Berechnung der Abfrage, erstmal erzeugen. Dazu verwenden wir wieder die gleiche Strategie, wie mit der zuvor umgesetzen Formel. | |||
<syntaxhighlight lang="asm"> | |||
@ if (2*(err-x) + 1 > 0) | |||
sub r10,r9,r7 | |||
lsl r10,#1 | |||
add r10,#1 | |||
cmp r10,#0 | |||
ble DrawCircleNo | |||
</syntaxhighlight> | |||
Wie zuvor, verwenden wir r10 als Zwischenspeicher vom Ergebnis aus "err-x", multiplizieren es mit zwei und addieren eins dazu. Das Ergebnis vergleichen wir mit Null. Hier bedeutet es, dass wenn das Ergebnis größer ist, dass der Inhalt der "IF-Anweisung" durchgeführt wird. Allerdings verwenden wir "kleiner oder gleich" und überspringen damit die entsprechenden Befehle, die in der "IF-Anweisung" durchgeführt werden sollen. Wenn das Ergebnis nicht "kleiner oder gleich" ist, also "größer", werden die nachfolgende Befehle durchgeführt, also wir befinden uns weiterhin in unserer "IF-Anweisung". | |||
Nun setzen wir den Inhalt der "IF-Anweisung" um: | |||
<syntaxhighlight lang="asm"> | |||
@ { | |||
@ x -= 1; | |||
sub r7,#1 | |||
@ err += 1 - 2*x; | |||
mov r10,r7 | |||
lsl r10,#1 | |||
mov r11,#1 | |||
sub r10,r11,r10 | |||
add r9,r9,r10 | |||
@ } | |||
DrawCircleNo: | |||
</syntaxhighlight> | |||
Damit ist der Inhalt der Whileschleife bearbeitet. | |||
Mit diesem Beispiel haben wir nun auch gesehen, wie wir Code aus anderen Hochsprachen umsetzen können. Gerade "C" ist eine Hochsprache, die schon ziemlich nah an Assembler ran reicht. Im Internet sind sehr viele Beispiele in "C" zu finden, die wir nicht neu erfinden müssen, sondern für unsere Zwecke entsprechend umschreiben können. | |||
Den Sourcecode für dieses Beispiel gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawCircle.zip DrawCircle.zip] | |||
== DrawFillCircle (Gefüllter Kreis) == | |||
Für einen gefüllten Kreis verwenden wir einfach die gleichen Funktionen, wie aus der zuvor erstellten Kreis-Funktion. Jede "putpixel" Unterfunktion defniert die Linien eines Achtel des Außenkreises. Um entsprechend festzustellen, welcher Teil, für welches Achtel zuständig ist, haben wir zunächst die Position des jeweiligen Achtels einer Uhr übertragen. Da diese Berechnungen jeweils Spiegelverkehr berechnet werden, können einfach aus den Koordinaten von oben nach unten mit Linien der Kreis gezeichnet werden. | |||
Als Beispiel das obere viertel: | |||
<syntaxhighlight lang="asm"> | |||
@ 1Uhr putpixel(x0 + y, y0 - x); | |||
add r0,r4,r8 | |||
sub r1,r5,r7 | |||
@ 11Uhr putpixel(x0 - y, y0 - x); | |||
sub r2,r4,r8 | |||
sub r3,r5,r7 | |||
bl DrawLine | |||
</syntaxhighlight> | |||
Den gesammten Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawFillCircle.zip DrawFillCircle.zip] | |||
== DrawChar (Zeichen) == | |||
Wir haben nun einige Zeichenfunktionen geschrieben, aber mächten wir nicht, dass uns der Raspberry Pi uns Informationen direkt auf den Bildschirm zeigt? Das wollen wir nun versuchen. In der Regel kommuniziert der Computer mit Schrift. Auch dass kann er nicht einfach so. Wie der Mensch, muss er zunächst lernen, wie jeder einzelne Buchstabe aussieht. Aber nicht nur das, sondern er muss auch wissen, wann er das entsprechende Zeichen verwenden soll. Wie wir bereits mit UART festgestellt hatten, stehen gewisse Bytewerte für genau bestimmte Zeichen oder Steuercodes. Dazu gibt es einen Standard, der diese Kommunikation festlegt und diesen haben wir auch bei der UART-Übermittlung verwendet. Dieser Code nennt sich "ANSI-Zeichencode". | |||
=== Der ANSI-Zeichencode === | |||
Der ANSI-Zeichencode, den wir hier verwenden werden, ist eine Erweiterung des ASCII-Codes mit der Umstellung von 7-Bit pro Zeichen auf 8-Bit pro Zeichen, welches 1 Byte entspricht. Der ANSI-Zeichencode wurde eingeführt, als Computeranwender über Netzwerke Kommunizieren wollten. Dazu war es notwendig, dass beide Computer mit dem gleichen Code sprechen. | |||
Innerhalb des ASCII-Codes sind die Zahlenwerte zum Zeichen in der Regel identisch, allerdings in der Erweiterung zu 8-Bit nicht wirklich gleich. Hierzu hat jede Computerfirma eigene Codepages entwickelt und veröffentlich. Gerade in der Anfangszeit überwiegten die Rechner von IBM mit dem Betriebssystem MS-DOS. Wir werden hier genau diesen Zeichencode verwenden, der dort eingesetzt wurde und immer noch auch unter Windows in der Konsole verwendet wird. Dieser Code wird als "Codepage 437" bezeichnet. | |||
[[Datei:ANSI.png|rand]] | |||
=== Font-Erstellen === | |||
Nun kümmern wir uns darum, dem Computer beizubringen, wie die einzelnen Zeichen aussehen. Dazu erstellen wir eine Art Datenbank, in der jedes Zeichen beschrieben wird, wie es aussieht.Wie bereits zuvor erwähnt, müssen wir selbst einen Zeichentabelle erstellen, in der wir festlegen, wie jedes einzelne Zeichen aussieht. Zur Einfachheit werden wir nur eine 8x8 Matrix verwenden. | |||
Dazu erstellen wir nun eine Datei, die dem folgendem Aussehen entspricht: | |||
<syntaxhighlight> | |||
Dimension | |||
INT X | |||
INT Y | |||
Data | |||
Binärcode | |||
</syntaxhighlight> | |||
Zunächst geben wir die Größe der Zeichen in X und Y an. Daraus können wir zur Berechnung der einzelnen Zeichen der entsprechenden Binärcode ausgelöst werden. Hier steht jedes Bit, das gesetzt ist, für ein Pixel, welches gezeichnet werden soll. | |||
Umgesetzt in Code, sieht es dann so aus: | |||
<syntaxhighlight lang="asm"> | |||
/* 8x8 FONT by Satyria */ | |||
@ Dimension | |||
.int 8 @ x | |||
.int 8 @ y | |||
@ Data | |||
@ 0: NULL | |||
.byte 0b00000000 | |||
.byte 0b00000000 | |||
.byte 0b00000000 | |||
.byte 0b00000000 | |||
.byte 0b00000000 | |||
.byte 0b00000000 | |||
.byte 0b00000000 | |||
.byte 0b00000000 | |||
... | |||
... | |||
@ 65: "A" | |||
.byte 0b00011000 | |||
.byte 0b00111100 | |||
.byte 0b00111100 | |||
.byte 0b01100110 | |||
.byte 0b01111110 | |||
.byte 0b11000011 | |||
.byte 0b11000011 | |||
.byte 0b00000000 | |||
@ 66: "B" | |||
... | |||
... | |||
</syntaxhighlight> | |||
=== Font-Anzeigen lassen === | |||
Nachdem wir nun einen Font beschrieben haben, also definiert haben, wie ein Zeichen aussieht, müssen wir diesen noch auf den Bildschirm zeichnen. | |||
Bisher können wir nicht auf externe Medien, während unser Programm läuft, zugreifen. Da dies bisher nicht geht, müssen wir den Font in unseren Sourcecode integrieren. Dazu verwenden wir die Include-Funktion des Compilers. | |||
<syntaxhighlight lang="asm"> | |||
.section .data | |||
font: | |||
.include "ms8x8font.fon" | |||
</syntaxhighlight> | |||
Unserer Funktion werden wir den Namen "DrawChar" geben. Ihr wird das Zeichen selbst, welches angezeigt werden soll und die Position (x,y) übergeben. | |||
Zunächst sichern wir diese Angaben, damit diese nicht gelöscht werden, wenn wir eine andere Funktion verwenden. | |||
<syntaxhighlight lang="asm"> | |||
DrawChar: | |||
/* | |||
void DrawChar(int Char, int x0, int y0) | |||
*/ | |||
push {r4,r5,r6,r7,r8,r9,r10,lr} | |||
mov r4,r0 @r4 = Char | |||
mov r5,r1 @r5 = x0 | |||
mov r6,r2 @r6 = y0 | |||
</syntaxhighlight> | |||
Unseren Font haben wir im Datenbereich abgelegt und ihm das Label "font" gegeben. Diesen Zeiger legen wir in r7 ab. | |||
<syntaxhighlight lang="asm"> | |||
ldr r7,=font | |||
add r7,#8 | |||
add r7,r0,lsl #3 | |||
</syntaxhighlight> | |||
Da wir die Dimension des Fonts kennen, überspringen wir diese Angabe. Danach multiblizieren wir die Übergabe des Zeichen, welche wir in r0 übergeben bekommen haben, mit acht. Ein Shift um 3 Positionen nach links, entspricht einer Multiplikation von acht. Und addieren das Ergebnis auf den Zeiger. Damit haben wir die Position des entsprechenden Fonts, den wir anzeigen lassen wollen. | |||
<syntaxhighlight lang="asm"> | |||
mov r9,r2 | |||
add r9,#8 | |||
</syntaxhighlight> | |||
Wir müssen, um den Font zu zeichnen, eine Schleife bilden, in der wir die Daten auswerten. Um eine Ende der Schleife zu definieren, verwenden wir die Y-Position. Da der Font 8 Pixel hoch ist, addieren wir einfach acht hinzu. Wenn wir später diesen Wert erreicht haben, haben wir auch das gesammte Zeichen gemahlt. | |||
Unser Font haben wir in Binärform erstellt. Das Bedeutet, wenn wir nun die Daten Auswerten, müssen wir hier auch in Binär denken. | |||
<syntaxhighlight lang="asm"> | |||
mov r10,#8 | |||
</syntaxhighlight> | |||
In r10 legen wir die Position des Pixels ab, den wir überprüfen und eventuell dann zeichnen werden. Damit wir einen einzelnen Bit prüfen können, verwenden wir r10 und eine Shift-Operation. | |||
Zunächst laden wir das gesammte Byte aus dem Speicher nach r0. | |||
<syntaxhighlight lang="asm"> | |||
DrawCharLoop1: | |||
ldrb r0,[r7] | |||
lsr r0,r10 | |||
and r0,#1 | |||
cmp r0,#1 | |||
</syntaxhighlight> | |||
Da wir in r10 die Position gespeichert haben, um die Position des entsprechenden Pixel auszuwerten, schieben wir einfach die Bits mit der Anzahl aus r10 nach rechts. Damit steht das eigentliche Bit, welches wir auswerten wollen, an der Position "0" im Byte. Damit der Wert dann noch auswertbar wird, verwenden wir ein "and" und löschen damit alle Bits oberhalb des unteren Bits. Nun können wir den Wert einfach mit #1 vergleichen. | |||
<syntaxhighlight lang="asm"> | |||
bne DrawCharSkip1 | |||
mov r0,r5 | |||
mov r1,r6 | |||
bl DrawPixel | |||
DrawCharSkip1: | |||
</syntaxhighlight> | |||
Wenn hier nun keine #1 steht, überspringen wir einfach die "DrawPixel"-Funktion. Ansonsten übergeben wir die Positionen von x und y und zeichnen einen Punkt. | |||
Wenn wir dies soweit haben, müssen wir unsere Werte, die wir für die Auswertung benutzen, entsprechend anpassen. | |||
<syntaxhighlight lang="asm"> | |||
add r5,#1 | |||
sub r10,#1 | |||
cmp r10,#-1 | |||
bne DrawCharSkip2 | |||
add r7,#1 | |||
mov r10,#8 | |||
sub r5,#9 | |||
add r6,#1 | |||
DrawCharSkip2: | |||
</syntaxhighlight> | |||
Zunächst erhöhen wir die Position X um eins, für das nächste Pixel. Subtrahieren die Shiftposition um eins und vergleichen dann diesen Wert. Wenn dieser nicht negativ wurde, überspringen wir den nächsten Teil. Sollte er negativ sein, so haben wir eine Zeile komplett ausgewertet. | |||
In diesem Fall, müssen wir einige Werte anpassen, damit wir dann die nächste Zeile anzeigen können. Dazu erhöhen wir zunächst den Zeiger auf den Font um eins, unseren Shiftwert setzen wir wieder auf acht und setzen X wieder zurück zum Ausgangswert. Y erhöhen wir um eins, für die nächste Zeile. | |||
Das nächste, was wir prüfen müssen, ist, wenn wir nun das Ende des Fonts erreicht haben. Dies Bedeutet, dass das komplette Zeichen angezeigt wurde. | |||
<syntaxhighlight lang="asm"> | |||
cmp r6,r9 | |||
popcs {r4,r5,r6,r7,r8,r9,r10,pc} | |||
b DrawCharLoop1 | |||
</syntaxhighlight> | |||
In r9 hatten wir das Ende des Fonts abgelegt und wir vergleichen nun einfach diese Position mit der aktuellen Y Position. Wenn dieser Unterschied Größer oder gleich ist, wird die Funktion beendet. Wenn nicht, wird einfach die Schleife wiederholt. | |||
Juhu, wir haben es geschafft. Nun wollen wir aber diese Funktion testen. Also schreiben wir unser Hauptprogramm um. | |||
<syntaxhighlight lang="asm"> | |||
main: | |||
mov sp,#0x8000 | |||
bl FB_Init | |||
mov r0,#0xff | |||
mov r1,#0x00 | |||
mov r2,#0xff | |||
bl SetColor | |||
</syntaxhighlight> | |||
Zunächst erstellen wir unseren Screen und setzen die Farbe auf Pink. | |||
Unser Ziel wird sein, dass wir alle Zeichen anzeigen lassen, die wir in der Font-Liberay erstellt haben. | |||
<syntaxhighlight lang="asm"> | |||
mov r4,#0 | |||
mov r5,#16 | |||
mov r6,#16 | |||
mov r7,#350 | |||
mov r8,#500 | |||
</syntaxhighlight> | |||
In r4 legen wir einen Zähler an, der jedes Zeichen repräsentiert. Zunächst wird der Wert auf null gesetzt, was dem ersten Zeichen entspricht. | |||
Um nicht aus dem Bereich der Anzeige zu kommen, werden wir die Zeichen in einem 16 x 16 Gitter setzen. Die Positionen sind in r5 und r6 abgelegt. | |||
In r7 und r8 sind die Startkoordinaten für unser Feld abgelegt. | |||
<syntaxhighlight lang="asm"> | |||
loop: | |||
mov r0,r4 | |||
mov r1,r7 | |||
mov r2,r8 | |||
bl DrawChar | |||
</syntaxhighlight> | |||
Mit einem einfache Übertrag vom Zeichen und der Position übergeben wir unserer neuen "DrawChar"-Funktion das Zeichen und lassen es uns anzeigen. | |||
<syntaxhighlight lang="asm"> | |||
add r4,#1 | |||
add r7,#10 | |||
sub r5,#1 | |||
cmp r5,#0 | |||
bne loop | |||
</syntaxhighlight> | |||
Im Anschluss erhöhen wir den Zeichenzähler (r4) um eins, Setzen die X Position um 10 hoch. Und reduzieren die X-Position unseres Gitters um eins. Solange dieser Wert nicht die Null erreicht hat, wird nun die Schleife ausgeführt. | |||
Wenn nicht: | |||
<syntaxhighlight lang="asm"> | |||
add r8,#10 | |||
mov r7,#350 | |||
mov r5,#16 | |||
sub r6,#1 | |||
cmp r6,#0 | |||
beq skip | |||
b loop | |||
skip: | |||
</syntaxhighlight> | |||
Die Position von Y wir um 10 erhöht und die Position X auf den Ausgangswert (350) gesetzt. In unserem Gitter fangen wir für die X-Position wieder von vorne an und die Y-Position wird um eins reduziert. Wenn nun die Y-Position null erreicht haben wir 256 Zeichen dargestellt und sind fertig. Wenn noch nicht, wird die Schleife einfach wiederholt. | |||
Den Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawChar.zip DrawChar.zip] | |||
== DrawString (Zeichenkette) == | |||
Wie bereits unter UART werden wir hier auch eine Stringfunktion erstellen, da wir in der Regel ganze Wörter oder Sätze anzeigen lassen wollen. | |||
Einen String, der in Assembler definiert wird, wird meist in der Section Data mit ".ascii" abgelegt. | |||
<syntaxhighlight lang="asm"> | |||
.section .data | |||
HelloText: | |||
.ascii "Hello World!" | |||
</syntaxhighlight> | |||
Wenn wir dies nun so lassen, erkennen wir kein Ende des Strings. Unter UART haben wir dazu noch die Länge des Strings definiert. Unter "C" wird allerdings ein anderes Verfahren verwendet, welches wir hier übernehmen werden. Unsere Stringfunktion wird bei jedem Zeichen prüfen, welches es anzeigen soll, ob hier ein Abschließende NULL steht. Sobald die kommt, weiß die Funktion, dass der String zu Ende ist. Zusätzlich fügen wir einen Code hinzu, der dem String eine neue Zeile angibt. | |||
<syntaxhighlight lang="asm"> | |||
.ascii "Hello World!\nNew Line\0" | |||
</syntaxhighlight> | |||
Diese zwei zusätzlichen Funktionen, müssen wir dann berücksichtigen. | |||
Kommen wir nun zu unsere neuen Funktion "DrawString" | |||
<syntaxhighlight lang="asm"> | |||
DrawString: | |||
/* | |||
void DrawString(int [String], int x0, int y0) | |||
*/ | |||
push {r4,r5,r6,r7,lr} | |||
mov r4,r0 | |||
mov r5,r1 | |||
mov r7,r1 | |||
mov r6,r2 | |||
</syntaxhighlight> | |||
Zunächst sichern wir wieder unsere Werte, damit diese nicht verloren gehen. In r4 steht der String, in r5 und r6 die Position. Zusätzlich, da wir auch eine NewLine-Funktion einbauen, sichern wir auch nochmal X in r7. | |||
Jetzt können wir in unseren Loop gehen und jedes Zeichen anzeigen lassen. | |||
<syntaxhighlight lang="asm"> | |||
DrawStringLoop: | |||
ldrb r0,[r4] | |||
cmp r0,#0 | |||
popeq {r4,r5,r6,r7,pc} | |||
</syntaxhighlight> | |||
Zunächst laden wir das Zeichen nach r0 und vergleichen es mit Null. Wie wir bereits zuvor festgelegt haben, bedeutet eine Null im String, das Ende des Strings. Also, wenn es so ist, beenden wir die Funktion. | |||
<syntaxhighlight lang="asm"> | |||
cmp r0,#'\n' | |||
moveq r5,r7 | |||
addeq r6,#8 | |||
addeq r4,#1 | |||
beq DrawStringLoop | |||
</syntaxhighlight> | |||
Als nächstes vergleichen wir den Wert mit dem Wert einer neuen Linie. Wenn dies der Fall ist, wir die X Position wieder auf den Startwert gesetzt und Position Y um 8 erhöht. Damit haben wir die neue Zeile erzeugt. Wir erhöhen noch den Zeiger im String um eins und lassen die Schleife von vorne beginnen. | |||
<syntaxhighlight lang="asm"> | |||
mov r1,r5 | |||
mov r2,r6 | |||
bl DrawChar | |||
add r4,#1 | |||
add r5,#8 | |||
b DrawStringLoop | |||
</syntaxhighlight> | |||
Wenn er nun hierher kommen ist, bedeutet es für uns, dass wir ein Zeichen anzeigen lassen können. Also übertragen wir die Y und X Position nach r1 und r2 und lassen uns das Zeichen anzeigen. Danach erhöhen wir den Zeiger im String um eins und erhöhen die X Position um 8 für das nächste Zeichen. Dann wiederholen wir alle Schritte wieder. | |||
In unserem Hauptprogramm können wir nun diese Funktion wie folgt verwenden. | |||
<syntaxhighlight lang="asm"> | |||
ldr r0,=HelloText | |||
mov r1,#600 | |||
mov r2,#500 | |||
bl DrawString | |||
... | |||
... | |||
.section .data | |||
HelloText: | |||
.ascii "Hello World!\nNew Line\0" | |||
</syntaxhighlight> | |||
In diesem Fall wird der Text | |||
<syntaxhighlight lang="asm"> | |||
Hello World! | |||
New Line | |||
</syntaxhighlight> | |||
an der Position 600x500 angezeigt. | |||
Mit dieser Art, einen String anzeigen zu lassen, sind viele Dinge möglich. Innerhalb des Font gibt es Zeichen, die als Rahmen verwendet werden können. Dies habe ich im Sourcecode zu diesem Teil umgesetzt. Schau einfach mal dort rein. | |||
Den Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawString.zip DrawString.zip] | |||
== Definierte Farben == | |||
Da ich selbst, das Programmieren auf einen Amiga erlernte, habe ich hier ein paar nützliche Dinge gelernt. Die Funktionslibaries sind hier ganz speziel abgelegt gewesen. Als Anwender hatte man eine Basisadresse an der zu bestimmten Libaries verwiesen wurden. Ein Teil dieser Libaries waren bereits beim Start des Betriebssystems im Speicher abgelegt. Andere Libaries konnten nachgeladen werden. Das Besondere an diesen Libaries war, dass diese am Anfang der Libarie nur Zeiger beinhaltete, die auf die eigentlichen Funktionen verwiesen. Dies hatte den Vorteil, dass egal, was mit den Funktionen passierte, die Ausgangsadresse gleich blieb. Damit konnte die Programme, die dort ausgeführt wurden, in der Regel immer wieder benutzt werden, auch wenn sich Funktionen veränderten. | |||
Wir werden diese Art der Funktionsweise nun verwenden, um Definierte Farben anzeigen zu lassen. Allerdings verwenden wir keine Funktionen, sondern definieren Farbwerte inklusive ihrer Namen. | |||
Die Namen der Farbwerte haben unterschiedliche längen, so dass ein einfaches Aufzählen und damit verbunden, die Berechnung, nicht möglich ist. Hier passt dann diese Art der "Libaries" genau ins Bild. | |||
Zunächst schreiben wir die Libarie und geben ihr den Namen "colors.h". Unsere Farbwerte sind von https://en.wikipedia.org/wiki/X11_color_names übernommen. | |||
<syntaxhighlight lang="asm"> | |||
.section .data | |||
SystemColors: | |||
Color_AliceBlue: | |||
.int _AliceBlue | |||
Color_AntiqueWhite: | |||
.int _AntiqueWhite | |||
Color_Aqua: | |||
.int _Aqua | |||
Color_Aquamarine: | |||
.int _Aquamarine | |||
Color_Azure: | |||
.int _Azure | |||
Color_Beige: | |||
.int _Beige | |||
... | |||
... | |||
... | |||
Color_Yellow: | |||
.int _Yellow | |||
Color_YellowGreen: | |||
.int _YellowGreen | |||
ColorEnd: | |||
.int 0 | |||
</syntaxhighlight> | |||
Im ersten Teil, der Datei, definieren wir die Farbe und setzen einfach eine Adresse, die Zeigt, wo den die eigentliche Farbe definiert ist. Dies machen wir für alle Farben. Am Schluss, der Liste wird noch ein NULL eingefügt, damit wir erkennen, wann diese Liste zu ende ist. | |||
<syntaxhighlight lang="asm"> | |||
.align 4 | |||
_AliceBlue: | |||
.int 0xF0 | |||
.int 0xF8 | |||
.int 0xFF | |||
.int _AliceBlueE-_AliceBlueB | |||
_AliceBlueB: | |||
.ascii "AliceBlue\0" | |||
_AliceBlueE: | |||
.align 4 | |||
_AntiqueWhite: | |||
.int 0xFA | |||
.int 0xEB | |||
.int 0xD7 | |||
.int _AntiqueWhiteE-_AntiqueWhiteB | |||
_AntiqueWhiteB: | |||
.ascii "AntiqueWhite\0" | |||
_AntiqueWhiteE: | |||
.align 4 | |||
_Aqua: | |||
... | |||
... | |||
... | |||
</syntaxhighlight> | |||
Im Anschluß definieren wir die einzelnen Farben. Dazu verwenden wir folgende Struktur: | |||
<syntaxhighlight> | |||
_Colorlabel: | |||
int Color_red | |||
int Color_green | |||
int Color_blue | |||
int Length of the string (name of the color) | |||
ascii "name of the color\0" | |||
</syntaxhighlight> | |||
Damit haben wir eine Color-Libarie erzeugt, die wir nun in unserem Hauptprogramm verwenden können. Damit wir etwas sehen, werden wir nun alle Farben mit ihren Namen anzeigen lassen. | |||
<syntaxhighlight lang="asm"> | |||
... | |||
.include "colors.h" | |||
... | |||
main: | |||
mov sp,#0x8000 | |||
bl FB_Init | |||
ldr r3,=Color_White | |||
ldr r3,[r3] | |||
ldr r0,[r3] | |||
ldr r1,[r3,#4] | |||
ldr r2,[r3,#8] | |||
bl SetColor | |||
mov r0,#0 @Box for black letters | |||
mov r1,#133 | |||
mov r2,#140 | |||
mov r3,#153 | |||
bl DrawFillBox | |||
</syntaxhighlight> | |||
Wenn wir den Bildschirm das erste mal anzeigen, bekommen wir einen schwarzen Bildschirm. Für die schwarze Schrift ist das etwas unglücklich, da schwarz auf schwarz sehr deutlich zu lesen ist. Also werden wir an einer Stelle des Screens eine weiße Box zeichnen, auf der später der schwarze String angezeigt wird. | |||
Gleich zu Beginn unseres Programmes verwenden wir unsere Libarie. Wir setzen den Zeiger der Farbe "Color_White" auf r3. Da hier nun der Zeiger auf die Struktur hinterlegt ist, laden wir genau diesen Zeiger nach r3. Nun können wir die Farben, aus dieser Liste nehmen und laden diese in r0 bis r2, damit die Funktion "SetColor" die entsprechenden Farbwerte übermittelt bekommt. | |||
Mit "DrawFillBox" zeichnen wir dann die weiße Box. | |||
Damit haben wir unsere Vorbereitungen gemacht. | |||
Für dieses Beispiel werden wir folgenden Pseudocode verwenden. | |||
<syntaxhighlight lang="C"> | |||
for a = 0 to 138 | |||
{ | |||
Load Color(a) | |||
Print String(a),x,y | |||
y=y+20 | |||
if y > ScreenY | |||
{ | |||
y = 0 | |||
x = x + 180 | |||
} | |||
} | |||
</syntaxhighlight> | |||
Zunächst setzen wir einige Ausgangswerte. | |||
<syntaxhighlight lang="asm"> | |||
mov r6,#0 | |||
mov r7,#0 | |||
mov r8,#0 | |||
ldr r9,=SystemColors @List of Colors | |||
</syntaxhighlight> | |||
Zunächst legen wir die Position x in r6 und y in r7. In r8 legen wir die entsprechende Farbe und in r9 speichern wir den Zeiger auf die Liste der Farben ab. | |||
Wie der Pseudocode beschreibt, verwenden wir eine "For"-Schleife. "For"-Schleifen scheinen einen einfachen Weg zu beschreiben, wie diese in Assembler umgesetzt werden könnten. Leider stimmt das nicht so ganz. Solche Schleifen werden immer durchgeführt, auch wenn schon beim ersten mal die Bedingungen nicht stimmen. Gleichzeitig müssen wir aber auch die Bedingungen auswerten. In Assembler können wir dies wie folgt umsetzen. | |||
<syntaxhighlight lang="asm"> | |||
b for_loop | |||
for_loop_intern: | |||
add r6,#20 @ Add y at 20 | |||
ldr r10,=scy @Load Adress for Screen (Y) | |||
ldr r10,[r10] @to r10 | |||
sub r10,#20 @Sub #20, because nothing can be shown after the end | |||
cmp r6,r10 @Compare with the end | |||
ble next_loop @If not, skip | |||
mov r6,#0 @set y to Null | |||
add r7,#180 @add x at 180 Pixel | |||
next_loop: | |||
add r8,#1 @Add counter by one | |||
for_loop: | |||
ldr r4,[r9] @Get address of the color | |||
add r9,#4 @Increase for next color | |||
ldr r0,[r4] @Load red | |||
add r4,#4 @next | |||
ldr r1,[r4] @Load green | |||
add r4,#4 @next | |||
ldr r2,[r4] @Load blue | |||
bl SetColor @Set color | |||
add r4,#8 @Skip length specification | |||
mov r0,r4 @Load Adress from String | |||
mov r1,r7 @Position x | |||
mov r2,r6 @and Position y | |||
bl DrawString @Show String | |||
cmp r8,#138 @Compare with number of system colors | |||
ble for_loop_intern @Not reached yet | |||
</syntaxhighlight> | |||
Zunächst überspringen wir unsere Berechnungen mit einem "Branch" und setzen die Farbe und lassen den Farbstring anzeigen. Anschließend vergleichen wir, ob das Ende erreicht wurde. Wenn nicht, kümmern wir uns um unsere Berechnungen, die wir zuvor übersprungen haben. | |||
Den gesammten Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/DrawColor.zip DrawColor.zip] | |||
----- | |||
{| style="width: 100%; | |||
| style="width: 33%;" | [[Die Anzeige|< Zurück (Die Anzeige)]] | |||
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]] | |||
| style="width: 33%; text-align:right;" | | |||
|} | |||
Aktuelle Version vom 22. August 2024, 10:12 Uhr
In diesem Kapitel werden wir einige Zeichenfunktionen erstellen. Am Ende können wir Linie, Rechtecke, Kreise und Zeichen anzeigen lassen und unser Ziel wird der hier dargestellte Bildschirm sein.
Um diesen Bildschirm anzeigen zu lassen, könnt ihr den folgenden Code 6.1.zip verwenden. In den folgenden Beispielen, wird nur auf die jeweilige Funktion eingegangen, die hier verwendet wurden. Die Funktion "DrawColor" wird gesondert behandelt.
Bisher können wir einen Punkt in einer bestimmten Farbe anzeigen. Da aber das Leben nicht nur aus Punkten besteht, werden wir ein paar Grafik Routinen erstellen, damit es einfacher wird. Als erstes werden wir eine Linie auf den Bildschirm zeichen.
DrawLine (Linie)
Vom Grunde her denkt man, dass das zeichnen von Linien relativ einfach wäre, aber das ist weit verfehlt. Überlege dir mal, wie du sowas angehen könntest. Die erste Überlegen, auf die man eingehen muss, ist das wir nur ein Koordinatensystem zu verfügen haben. Solange wir nur 90° Winkel benutzen scheint es relativ einfach zu sein. Aber sobald eine Linie einen anderen Winkel verwendet, wird es schwieriger.
Bei der nächsten Überlegung kommt man auf Formeln, die Multiplikationen oder Divisionen verwendet. Damit könnte man bestimmte Steigungen berechnen. Multiplikationen könnte man noch verwenden, wenn hier der Zeitfaktor keine Rolle spielt. Divisionen werden aber von der CPU ohne Co-Prozessor nicht unterstützt.
Aus diesem Grund müssen wir hier einen anderen Weg finden.
Nach Recherche im Internet (warum alles neu erfinden) habe ich den "Bresenhams Algorithmus" gefunden, wie er auch auf der Seite “Baking Pi – Operating Systems Development“ im Kapitel “Lesson 7 Screen02” verwendet wird. Also verwenden wir diese und schreiben diese für unser System um.
Zunächst sichern wir die Position von Anfang und Ende der Linie.
DrawLine:
push {r4,r5,r6,r7,r8,r9,r10,r11,r12,lr}
mov r9,r0 @r9 = x1
mov r10,r2 @r10 = x2
mov r11,r1 @r11 = y1
mov r12,r3 @r12 = y2
Wir müssen zunächst DeltaX und das StepX berechnen. Da es von der Reihenfolge, größe des Wertes der Reihenfolge abhängig ist, vergleichen wir beide x-Positionen und reagieren entsprechend.
cmp r9,r10 @Compare x1 with x2
@If x1 is greater than x2
subgt r4,r9,r10 @DeltaX = x1 - x2
movgt r6,#-1 @StepX = -1
@Else (x1 less than / equal to x2)
suble r4,r10,r9 @DeltaX = x2 - x1
movle r6,#1 @StepX = 1
Das gleiche machen wir für die Y-Position.
cmp r11,r12 @Compare y1 with y2
@If y1 greater than x2
subgt r5,r12,r11 @DeltaY = y1 - y2
movgt r7,#-1 @StepY = -1
@Else (y1 less than / equal to y2)
suble r5,r11,r12 @DeltaY = y2 - y1
movle r7,#1 @StepY = 1
Als nächstes setzen wir die Ausgangswerte.
add r8,r4,r5 @Error = DeltaX + DeltaY
add r10,r6 @x2 = x2 + StepX
add r12,r7 @y2 = y2 + StepY
Nun können wir die Linie zeichnen. Innerhalb der Schleife, die wir durchführen, prüfen wir, ob x1 und x2 oder y1 und y2 identisch sind. Dies würde die Schleife beenden und gleichzeitig auch die Funktion.
DrawLineLoop$:
teq r9,r10 @Compare x1 with x2
teqne r11,r12 @if unequal; Compare y1 with y2
popeq {r4,r5,r6,r7,r8,r9,r10,r11,r12,pc} @if equal then end
Als nächstes laden wir die aktuelle Position und zeichnen einen Punkt.
mov r0,r9 @copy x1 to r0
mov r1,r11 @copy y1 to r1
bl DrawPixel
Über DeltaY und “Fehler x 2” wird festgestellt ob x1 verändert wird, sowie für DeltaX und “Fehler x 2” für y1. Danach kann die Schleife wiederholt werden.
cmp r5, r8,lsl #1 @If DeltaY is less or equal than (error * 2)
addle r8,r5 @error = error + DeltaY
addle r9,r6 @x1 = x1 + StepX
cmp r4, r8,lsl #1 @If DeltaX greater than (error * 2)
addge r8,r4 @error = error + DeltaX
addge r11,r7 @y1 = y1 + StepY
b DrawLineLoop$ @calculate next point
Wie sieht es nun im Hauptprogramm aus?
Im Hauptprogramm müssen wir zunächst alle Includes laden, die wir verwenden, definieren unseren Screen und starten unser Programm.
.include "base.inc"
.include "screen.h"
.include "drawing.h"
.equ SCREEN_X, 1920
.equ SCREEN_Y, 1080
.equ BITS_PER_PIXEL, 32
.section .init
.globl _start
_start:
b main
.section .text
main:
mov sp,#0x8000
Den Sourcecode gibt es hier: DrawLine.zip
Neue Befehle (1)
subgt, movgt
sub und mov sind bereits bekannt. Allerdings wurde ihnen der Bedingungscode für größer mit Vorzeichen übergeben. Dies bedeutet, dass diese nur durchgeführt werden, wenn zuvor das Ergebnis entsprechend ausfiel.
suble, movle, addle
Auch diese Befehle hatten wir bereits. Das Suffix “le” bedeutet, das der Befehl nur Ausgeführt wird, wenn das Ergebnis kleiner oder gleich mit Vorzeichen war.
teq, teqne
Dieser Befehl testet zwei Werte und aktualisiert die Bedingsflags. Allerdings wird das Ergebnis nicht in ein Register geschrieben, wie es CMP macht. Der Suffix "ne" bedeutet, dass der Befehl nur ausgeführt wird, wenn das zuvor festgestellte Ergebnis nicht gleich war.
popeq
Setzt die Daten aus dem Stack zurück in die angegebenen Register, wenn zuvor das Ergebniss “gleich” war.
addge
Befehl wird ausgeführt, wenn das Ergebnis zuvor Größer oder gleich (mit Vorzeichen) war.
DrawBox (Rechteck)
Nachdem wir nun Linie zeichnen können, ist es relativ einfach, eine Rechteck zu zeichnen. Wir verbinden einfach die Ecken miteinander, und wir haben unser Rechteck.
DrawBox:
/*
void DrawBox (x0, y0, x1, y1)
*/
push {r4,r5,r6,r7,lr}
mov r4,r0 @r4 = x0
mov r5,r1 @r5 = y0
mov r6,r2 @r6 = x1
mov r7,r3 @r7 = y1
mov r0,r4
mov r1,r5
mov r2,r6
mov r3,r5
bl DrawLine
mov r0,r6
mov r1,r5
mov r2,r6
mov r3,r7
bl DrawLine
mov r0,r6
mov r1,r7
mov r2,r4
mov r3,r7
bl DrawLine
mov r0,r4
mov r1,r7
mov r2,r4
mov r3,r5
bl DrawLine
pop {r4,r5,r6,r7,pc}
Den Sourcecode gibt es hier: DrawBox.zip
DrawFillBox (Gefülltes Rechteck)
Wenn wir nun ein gefülltes Rechteck erstellen wollen, so zeichnen wir einfach ein Linie neben der anderen. Wir verwenden dafür einfach "y0" und erhöhen y0 um 1 und vergleichen es mit y1. Wenn wir diesen Wert erreicht haben, sind wir fertig.
DrawFillBox:
/*
void DrawFillBox (x0, y0, x1, y1)
*/
push {r4,r5,r6,r7,lr}
mov r4,r0 @r4 = x0
mov r5,r1 @r5 = y0
mov r6,r2 @r6 = x1
mov r7,r3 @r7 = y1
DrawFillBoxLoop:
mov r0,r4
mov r1,r5
mov r2,r6
mov r3,r5
bl DrawLine
add r5,#1
cmp r5,r7
ble DrawFillBoxLoop
pop {r4,r5,r6,r7,pc}
Den Sourcecode gibt es hier: DrawFillBox.zip
DrawCircle (Kreis)
Wenn man sich in der Mathematik gut auskennt, kennt man die Funktionen für SINUS und COSINUS. Mit beiden Funktionen wäre es relativ einfach, einen Kreis zu erzeugen. Leider haben wir zu diesem Zeitpunkt keine Cosinus oder Sinusfunktion. Zusätzlich sollten wir, wie bei den Linien auch, auf Divisionen verzichten. Wenn man etwas im Internet forscht, findet man den Bresenham’s Kreis Algorithmus. Diesen werden wir hier nun verwenden. Mehr informationen zu diesem Algorithnus findet man zum Beispiel unter https://de.wikipedia.org/wiki/Bresenham-Algorithmus.
Folgenden Pseudo-C-Code verwenden wir für unsere Routine:
DrawCircle:
/*
void DrawCircle(int x0, int y0, int radius)
*/
int x = radius;
int y = 0;
int err = 0;
while (x >= y)
{
putpixel(x0 + x, y0 + y);
putpixel(x0 + y, y0 + x);
putpixel(x0 - y, y0 + x);
putpixel(x0 - x, y0 + y);
putpixel(x0 - x, y0 - y);
putpixel(x0 - y, y0 - x);
putpixel(x0 + y, y0 - x);
putpixel(x0 + x, y0 - y);
y += 1;
err += 1 + 2*y;
if (2*(err-x) + 1 > 0)
{
x -= 1;
err += 1 - 2*x;
}
}
Also Wandeln wir einfach diesen Code in Assembler um.
Zunächst sichern wir wieder unsere Übergabewerte.
DrawCircle:
/*
void DrawCircle(int x0, int y0, int radius)
*/
push {r4,r5,r6,r7,r8,r9,r10,r11,lr}
mov r4,r0 @r4 = x0
mov r5,r1 @r5 = y0
mov r6,r2 @r6 = radius
Und setzen die Ausgangswerte für unsere Funktion.
@ int x = radius;
mov r7,r6 @r7 = X
@ int y = 0;
mov r8,#0 @r8 = y
@ int err = 0;
mov r9,#0 @r9 = err
Als nächstes wird im C-Code eine While-Schleife definiert. Dies bedeutet, dass die Schleife solange durchgeführt wird, wie das Ergebnis wahr ist. Sobald das Ergebnis falsch ist, wird die Schleife abgebrochen. Damit erstellen wir nun in unserem Code auch eine Schleife.
DrawCircleLoop:
@ while (x >= y)
@ {
...
...
@ }
cmp r7,r8
bcs DrawCircleLoop
Für die entsprechende "putpixel" Funktion verwenden wir unsere "DrawPixel"-Funktion. Allerdings werden dir Koordinaten mittels einer Berechnung der Funktion übergeben. Diese Berechnung müssen wir zuvor durchführen und geben dann das Ergebnis unserer "DrawPixel"-Funktion.
@ putpixel(x0 + x, y0 + y);
add r0,r4,r7
add r1,r5,r8
bl DrawPixel
@ putpixel(x0 + y, y0 + x);
add r0,r4,r8
add r1,r5,r7
bl DrawPixel
...
...
Nun setzen wir folgende Berechnung in Assembler um:
y += 1;
err += 1 + 2*y;
if (2*(err-x) + 1 > 0)
{
x -= 1;
err += 1 - 2*x;
}
y += 1; Dieser Code in "C" bedeutet das gleiche wie y = y + 1. Also y wird einfach um eins erhöht. Umgesetzt, da wir "r8" als y definiert haben, addieren wir einfach 1 mit "r8".
@ y += 1;
add r8,#1
Die nächste Zeile err += 1 + 2*y; nehmen wir mal auseinander.
Zunächst haben wir die Berechnung 2*y. Eine Multiplikation um 2 können wir mit einem "lsl" durchführen, wie wir bereits zuvor gelernt haben. Das Zwischenergebnis legen wir in r10 ab.
mov r10,r8,lsl #1
Bei der nächsten Berechnung wird einfach nur eins addiert. Dies dürfte für uns auch kein Problem sein:
add r10,#1
err += bedeutet ja wieder, dass wir das Ergebnis hinzuaddieren müssen. Für err verwenden wir das Register r9.
add r9,r9,r10
Komplett entspricht es diesen Code:
@ err += 1 + 2*y;
mov r10,r8,lsl #1
add r10,#1
add r9,r9,r10 @ r9 = err
Die nächste Anweisung, die wir umsetzen müssen, ist eine IF-Anweisung. IFs können wir mit bedingten Branch durchführen. Allerdings müssen wir zuvor die entsprechende Abfrage, hier die Berechnung der Abfrage, erstmal erzeugen. Dazu verwenden wir wieder die gleiche Strategie, wie mit der zuvor umgesetzen Formel.
@ if (2*(err-x) + 1 > 0)
sub r10,r9,r7
lsl r10,#1
add r10,#1
cmp r10,#0
ble DrawCircleNo
Wie zuvor, verwenden wir r10 als Zwischenspeicher vom Ergebnis aus "err-x", multiplizieren es mit zwei und addieren eins dazu. Das Ergebnis vergleichen wir mit Null. Hier bedeutet es, dass wenn das Ergebnis größer ist, dass der Inhalt der "IF-Anweisung" durchgeführt wird. Allerdings verwenden wir "kleiner oder gleich" und überspringen damit die entsprechenden Befehle, die in der "IF-Anweisung" durchgeführt werden sollen. Wenn das Ergebnis nicht "kleiner oder gleich" ist, also "größer", werden die nachfolgende Befehle durchgeführt, also wir befinden uns weiterhin in unserer "IF-Anweisung".
Nun setzen wir den Inhalt der "IF-Anweisung" um:
@ {
@ x -= 1;
sub r7,#1
@ err += 1 - 2*x;
mov r10,r7
lsl r10,#1
mov r11,#1
sub r10,r11,r10
add r9,r9,r10
@ }
DrawCircleNo:
Damit ist der Inhalt der Whileschleife bearbeitet.
Mit diesem Beispiel haben wir nun auch gesehen, wie wir Code aus anderen Hochsprachen umsetzen können. Gerade "C" ist eine Hochsprache, die schon ziemlich nah an Assembler ran reicht. Im Internet sind sehr viele Beispiele in "C" zu finden, die wir nicht neu erfinden müssen, sondern für unsere Zwecke entsprechend umschreiben können.
Den Sourcecode für dieses Beispiel gibt es hier: DrawCircle.zip
DrawFillCircle (Gefüllter Kreis)
Für einen gefüllten Kreis verwenden wir einfach die gleichen Funktionen, wie aus der zuvor erstellten Kreis-Funktion. Jede "putpixel" Unterfunktion defniert die Linien eines Achtel des Außenkreises. Um entsprechend festzustellen, welcher Teil, für welches Achtel zuständig ist, haben wir zunächst die Position des jeweiligen Achtels einer Uhr übertragen. Da diese Berechnungen jeweils Spiegelverkehr berechnet werden, können einfach aus den Koordinaten von oben nach unten mit Linien der Kreis gezeichnet werden.
Als Beispiel das obere viertel:
@ 1Uhr putpixel(x0 + y, y0 - x);
add r0,r4,r8
sub r1,r5,r7
@ 11Uhr putpixel(x0 - y, y0 - x);
sub r2,r4,r8
sub r3,r5,r7
bl DrawLine
Den gesammten Sourcecode gibt es hier: DrawFillCircle.zip
DrawChar (Zeichen)
Wir haben nun einige Zeichenfunktionen geschrieben, aber mächten wir nicht, dass uns der Raspberry Pi uns Informationen direkt auf den Bildschirm zeigt? Das wollen wir nun versuchen. In der Regel kommuniziert der Computer mit Schrift. Auch dass kann er nicht einfach so. Wie der Mensch, muss er zunächst lernen, wie jeder einzelne Buchstabe aussieht. Aber nicht nur das, sondern er muss auch wissen, wann er das entsprechende Zeichen verwenden soll. Wie wir bereits mit UART festgestellt hatten, stehen gewisse Bytewerte für genau bestimmte Zeichen oder Steuercodes. Dazu gibt es einen Standard, der diese Kommunikation festlegt und diesen haben wir auch bei der UART-Übermittlung verwendet. Dieser Code nennt sich "ANSI-Zeichencode".
Der ANSI-Zeichencode
Der ANSI-Zeichencode, den wir hier verwenden werden, ist eine Erweiterung des ASCII-Codes mit der Umstellung von 7-Bit pro Zeichen auf 8-Bit pro Zeichen, welches 1 Byte entspricht. Der ANSI-Zeichencode wurde eingeführt, als Computeranwender über Netzwerke Kommunizieren wollten. Dazu war es notwendig, dass beide Computer mit dem gleichen Code sprechen.
Innerhalb des ASCII-Codes sind die Zahlenwerte zum Zeichen in der Regel identisch, allerdings in der Erweiterung zu 8-Bit nicht wirklich gleich. Hierzu hat jede Computerfirma eigene Codepages entwickelt und veröffentlich. Gerade in der Anfangszeit überwiegten die Rechner von IBM mit dem Betriebssystem MS-DOS. Wir werden hier genau diesen Zeichencode verwenden, der dort eingesetzt wurde und immer noch auch unter Windows in der Konsole verwendet wird. Dieser Code wird als "Codepage 437" bezeichnet.
Font-Erstellen
Nun kümmern wir uns darum, dem Computer beizubringen, wie die einzelnen Zeichen aussehen. Dazu erstellen wir eine Art Datenbank, in der jedes Zeichen beschrieben wird, wie es aussieht.Wie bereits zuvor erwähnt, müssen wir selbst einen Zeichentabelle erstellen, in der wir festlegen, wie jedes einzelne Zeichen aussieht. Zur Einfachheit werden wir nur eine 8x8 Matrix verwenden.
Dazu erstellen wir nun eine Datei, die dem folgendem Aussehen entspricht:
Dimension
INT X
INT Y
Data
BinärcodeZunächst geben wir die Größe der Zeichen in X und Y an. Daraus können wir zur Berechnung der einzelnen Zeichen der entsprechenden Binärcode ausgelöst werden. Hier steht jedes Bit, das gesetzt ist, für ein Pixel, welches gezeichnet werden soll. Umgesetzt in Code, sieht es dann so aus:
/* 8x8 FONT by Satyria */
@ Dimension
.int 8 @ x
.int 8 @ y
@ Data
@ 0: NULL
.byte 0b00000000
.byte 0b00000000
.byte 0b00000000
.byte 0b00000000
.byte 0b00000000
.byte 0b00000000
.byte 0b00000000
.byte 0b00000000
...
...
@ 65: "A"
.byte 0b00011000
.byte 0b00111100
.byte 0b00111100
.byte 0b01100110
.byte 0b01111110
.byte 0b11000011
.byte 0b11000011
.byte 0b00000000
@ 66: "B"
...
...
Font-Anzeigen lassen
Nachdem wir nun einen Font beschrieben haben, also definiert haben, wie ein Zeichen aussieht, müssen wir diesen noch auf den Bildschirm zeichnen.
Bisher können wir nicht auf externe Medien, während unser Programm läuft, zugreifen. Da dies bisher nicht geht, müssen wir den Font in unseren Sourcecode integrieren. Dazu verwenden wir die Include-Funktion des Compilers.
.section .data
font:
.include "ms8x8font.fon"
Unserer Funktion werden wir den Namen "DrawChar" geben. Ihr wird das Zeichen selbst, welches angezeigt werden soll und die Position (x,y) übergeben.
Zunächst sichern wir diese Angaben, damit diese nicht gelöscht werden, wenn wir eine andere Funktion verwenden.
DrawChar:
/*
void DrawChar(int Char, int x0, int y0)
*/
push {r4,r5,r6,r7,r8,r9,r10,lr}
mov r4,r0 @r4 = Char
mov r5,r1 @r5 = x0
mov r6,r2 @r6 = y0
Unseren Font haben wir im Datenbereich abgelegt und ihm das Label "font" gegeben. Diesen Zeiger legen wir in r7 ab.
ldr r7,=font
add r7,#8
add r7,r0,lsl #3
Da wir die Dimension des Fonts kennen, überspringen wir diese Angabe. Danach multiblizieren wir die Übergabe des Zeichen, welche wir in r0 übergeben bekommen haben, mit acht. Ein Shift um 3 Positionen nach links, entspricht einer Multiplikation von acht. Und addieren das Ergebnis auf den Zeiger. Damit haben wir die Position des entsprechenden Fonts, den wir anzeigen lassen wollen.
mov r9,r2
add r9,#8
Wir müssen, um den Font zu zeichnen, eine Schleife bilden, in der wir die Daten auswerten. Um eine Ende der Schleife zu definieren, verwenden wir die Y-Position. Da der Font 8 Pixel hoch ist, addieren wir einfach acht hinzu. Wenn wir später diesen Wert erreicht haben, haben wir auch das gesammte Zeichen gemahlt.
Unser Font haben wir in Binärform erstellt. Das Bedeutet, wenn wir nun die Daten Auswerten, müssen wir hier auch in Binär denken.
mov r10,#8
In r10 legen wir die Position des Pixels ab, den wir überprüfen und eventuell dann zeichnen werden. Damit wir einen einzelnen Bit prüfen können, verwenden wir r10 und eine Shift-Operation.
Zunächst laden wir das gesammte Byte aus dem Speicher nach r0.
DrawCharLoop1:
ldrb r0,[r7]
lsr r0,r10
and r0,#1
cmp r0,#1
Da wir in r10 die Position gespeichert haben, um die Position des entsprechenden Pixel auszuwerten, schieben wir einfach die Bits mit der Anzahl aus r10 nach rechts. Damit steht das eigentliche Bit, welches wir auswerten wollen, an der Position "0" im Byte. Damit der Wert dann noch auswertbar wird, verwenden wir ein "and" und löschen damit alle Bits oberhalb des unteren Bits. Nun können wir den Wert einfach mit #1 vergleichen.
bne DrawCharSkip1
mov r0,r5
mov r1,r6
bl DrawPixel
DrawCharSkip1:
Wenn hier nun keine #1 steht, überspringen wir einfach die "DrawPixel"-Funktion. Ansonsten übergeben wir die Positionen von x und y und zeichnen einen Punkt.
Wenn wir dies soweit haben, müssen wir unsere Werte, die wir für die Auswertung benutzen, entsprechend anpassen.
add r5,#1
sub r10,#1
cmp r10,#-1
bne DrawCharSkip2
add r7,#1
mov r10,#8
sub r5,#9
add r6,#1
DrawCharSkip2:
Zunächst erhöhen wir die Position X um eins, für das nächste Pixel. Subtrahieren die Shiftposition um eins und vergleichen dann diesen Wert. Wenn dieser nicht negativ wurde, überspringen wir den nächsten Teil. Sollte er negativ sein, so haben wir eine Zeile komplett ausgewertet. In diesem Fall, müssen wir einige Werte anpassen, damit wir dann die nächste Zeile anzeigen können. Dazu erhöhen wir zunächst den Zeiger auf den Font um eins, unseren Shiftwert setzen wir wieder auf acht und setzen X wieder zurück zum Ausgangswert. Y erhöhen wir um eins, für die nächste Zeile.
Das nächste, was wir prüfen müssen, ist, wenn wir nun das Ende des Fonts erreicht haben. Dies Bedeutet, dass das komplette Zeichen angezeigt wurde.
cmp r6,r9
popcs {r4,r5,r6,r7,r8,r9,r10,pc}
b DrawCharLoop1
In r9 hatten wir das Ende des Fonts abgelegt und wir vergleichen nun einfach diese Position mit der aktuellen Y Position. Wenn dieser Unterschied Größer oder gleich ist, wird die Funktion beendet. Wenn nicht, wird einfach die Schleife wiederholt.
Juhu, wir haben es geschafft. Nun wollen wir aber diese Funktion testen. Also schreiben wir unser Hauptprogramm um.
main:
mov sp,#0x8000
bl FB_Init
mov r0,#0xff
mov r1,#0x00
mov r2,#0xff
bl SetColor
Zunächst erstellen wir unseren Screen und setzen die Farbe auf Pink.
Unser Ziel wird sein, dass wir alle Zeichen anzeigen lassen, die wir in der Font-Liberay erstellt haben.
mov r4,#0
mov r5,#16
mov r6,#16
mov r7,#350
mov r8,#500
In r4 legen wir einen Zähler an, der jedes Zeichen repräsentiert. Zunächst wird der Wert auf null gesetzt, was dem ersten Zeichen entspricht. Um nicht aus dem Bereich der Anzeige zu kommen, werden wir die Zeichen in einem 16 x 16 Gitter setzen. Die Positionen sind in r5 und r6 abgelegt. In r7 und r8 sind die Startkoordinaten für unser Feld abgelegt.
loop:
mov r0,r4
mov r1,r7
mov r2,r8
bl DrawChar
Mit einem einfache Übertrag vom Zeichen und der Position übergeben wir unserer neuen "DrawChar"-Funktion das Zeichen und lassen es uns anzeigen.
add r4,#1
add r7,#10
sub r5,#1
cmp r5,#0
bne loop
Im Anschluss erhöhen wir den Zeichenzähler (r4) um eins, Setzen die X Position um 10 hoch. Und reduzieren die X-Position unseres Gitters um eins. Solange dieser Wert nicht die Null erreicht hat, wird nun die Schleife ausgeführt.
Wenn nicht:
add r8,#10
mov r7,#350
mov r5,#16
sub r6,#1
cmp r6,#0
beq skip
b loop
skip:
Die Position von Y wir um 10 erhöht und die Position X auf den Ausgangswert (350) gesetzt. In unserem Gitter fangen wir für die X-Position wieder von vorne an und die Y-Position wird um eins reduziert. Wenn nun die Y-Position null erreicht haben wir 256 Zeichen dargestellt und sind fertig. Wenn noch nicht, wird die Schleife einfach wiederholt.
Den Sourcecode gibt es hier: DrawChar.zip
DrawString (Zeichenkette)
Wie bereits unter UART werden wir hier auch eine Stringfunktion erstellen, da wir in der Regel ganze Wörter oder Sätze anzeigen lassen wollen.
Einen String, der in Assembler definiert wird, wird meist in der Section Data mit ".ascii" abgelegt.
.section .data
HelloText:
.ascii "Hello World!"
Wenn wir dies nun so lassen, erkennen wir kein Ende des Strings. Unter UART haben wir dazu noch die Länge des Strings definiert. Unter "C" wird allerdings ein anderes Verfahren verwendet, welches wir hier übernehmen werden. Unsere Stringfunktion wird bei jedem Zeichen prüfen, welches es anzeigen soll, ob hier ein Abschließende NULL steht. Sobald die kommt, weiß die Funktion, dass der String zu Ende ist. Zusätzlich fügen wir einen Code hinzu, der dem String eine neue Zeile angibt.
.ascii "Hello World!\nNew Line\0"
Diese zwei zusätzlichen Funktionen, müssen wir dann berücksichtigen.
Kommen wir nun zu unsere neuen Funktion "DrawString"
DrawString:
/*
void DrawString(int [String], int x0, int y0)
*/
push {r4,r5,r6,r7,lr}
mov r4,r0
mov r5,r1
mov r7,r1
mov r6,r2
Zunächst sichern wir wieder unsere Werte, damit diese nicht verloren gehen. In r4 steht der String, in r5 und r6 die Position. Zusätzlich, da wir auch eine NewLine-Funktion einbauen, sichern wir auch nochmal X in r7.
Jetzt können wir in unseren Loop gehen und jedes Zeichen anzeigen lassen.
DrawStringLoop:
ldrb r0,[r4]
cmp r0,#0
popeq {r4,r5,r6,r7,pc}
Zunächst laden wir das Zeichen nach r0 und vergleichen es mit Null. Wie wir bereits zuvor festgelegt haben, bedeutet eine Null im String, das Ende des Strings. Also, wenn es so ist, beenden wir die Funktion.
cmp r0,#'\n'
moveq r5,r7
addeq r6,#8
addeq r4,#1
beq DrawStringLoop
Als nächstes vergleichen wir den Wert mit dem Wert einer neuen Linie. Wenn dies der Fall ist, wir die X Position wieder auf den Startwert gesetzt und Position Y um 8 erhöht. Damit haben wir die neue Zeile erzeugt. Wir erhöhen noch den Zeiger im String um eins und lassen die Schleife von vorne beginnen.
mov r1,r5
mov r2,r6
bl DrawChar
add r4,#1
add r5,#8
b DrawStringLoop
Wenn er nun hierher kommen ist, bedeutet es für uns, dass wir ein Zeichen anzeigen lassen können. Also übertragen wir die Y und X Position nach r1 und r2 und lassen uns das Zeichen anzeigen. Danach erhöhen wir den Zeiger im String um eins und erhöhen die X Position um 8 für das nächste Zeichen. Dann wiederholen wir alle Schritte wieder.
In unserem Hauptprogramm können wir nun diese Funktion wie folgt verwenden.
ldr r0,=HelloText
mov r1,#600
mov r2,#500
bl DrawString
...
...
.section .data
HelloText:
.ascii "Hello World!\nNew Line\0"
In diesem Fall wird der Text
Hello World!
New Line
an der Position 600x500 angezeigt.
Mit dieser Art, einen String anzeigen zu lassen, sind viele Dinge möglich. Innerhalb des Font gibt es Zeichen, die als Rahmen verwendet werden können. Dies habe ich im Sourcecode zu diesem Teil umgesetzt. Schau einfach mal dort rein.
Den Sourcecode gibt es hier: DrawString.zip
Definierte Farben
Da ich selbst, das Programmieren auf einen Amiga erlernte, habe ich hier ein paar nützliche Dinge gelernt. Die Funktionslibaries sind hier ganz speziel abgelegt gewesen. Als Anwender hatte man eine Basisadresse an der zu bestimmten Libaries verwiesen wurden. Ein Teil dieser Libaries waren bereits beim Start des Betriebssystems im Speicher abgelegt. Andere Libaries konnten nachgeladen werden. Das Besondere an diesen Libaries war, dass diese am Anfang der Libarie nur Zeiger beinhaltete, die auf die eigentlichen Funktionen verwiesen. Dies hatte den Vorteil, dass egal, was mit den Funktionen passierte, die Ausgangsadresse gleich blieb. Damit konnte die Programme, die dort ausgeführt wurden, in der Regel immer wieder benutzt werden, auch wenn sich Funktionen veränderten.
Wir werden diese Art der Funktionsweise nun verwenden, um Definierte Farben anzeigen zu lassen. Allerdings verwenden wir keine Funktionen, sondern definieren Farbwerte inklusive ihrer Namen.
Die Namen der Farbwerte haben unterschiedliche längen, so dass ein einfaches Aufzählen und damit verbunden, die Berechnung, nicht möglich ist. Hier passt dann diese Art der "Libaries" genau ins Bild.
Zunächst schreiben wir die Libarie und geben ihr den Namen "colors.h". Unsere Farbwerte sind von https://en.wikipedia.org/wiki/X11_color_names übernommen.
.section .data
SystemColors:
Color_AliceBlue:
.int _AliceBlue
Color_AntiqueWhite:
.int _AntiqueWhite
Color_Aqua:
.int _Aqua
Color_Aquamarine:
.int _Aquamarine
Color_Azure:
.int _Azure
Color_Beige:
.int _Beige
...
...
...
Color_Yellow:
.int _Yellow
Color_YellowGreen:
.int _YellowGreen
ColorEnd:
.int 0
Im ersten Teil, der Datei, definieren wir die Farbe und setzen einfach eine Adresse, die Zeigt, wo den die eigentliche Farbe definiert ist. Dies machen wir für alle Farben. Am Schluss, der Liste wird noch ein NULL eingefügt, damit wir erkennen, wann diese Liste zu ende ist.
.align 4
_AliceBlue:
.int 0xF0
.int 0xF8
.int 0xFF
.int _AliceBlueE-_AliceBlueB
_AliceBlueB:
.ascii "AliceBlue\0"
_AliceBlueE:
.align 4
_AntiqueWhite:
.int 0xFA
.int 0xEB
.int 0xD7
.int _AntiqueWhiteE-_AntiqueWhiteB
_AntiqueWhiteB:
.ascii "AntiqueWhite\0"
_AntiqueWhiteE:
.align 4
_Aqua:
...
...
...
Im Anschluß definieren wir die einzelnen Farben. Dazu verwenden wir folgende Struktur:
_Colorlabel:
int Color_red
int Color_green
int Color_blue
int Length of the string (name of the color)
ascii "name of the color\0"Damit haben wir eine Color-Libarie erzeugt, die wir nun in unserem Hauptprogramm verwenden können. Damit wir etwas sehen, werden wir nun alle Farben mit ihren Namen anzeigen lassen.
...
.include "colors.h"
...
main:
mov sp,#0x8000
bl FB_Init
ldr r3,=Color_White
ldr r3,[r3]
ldr r0,[r3]
ldr r1,[r3,#4]
ldr r2,[r3,#8]
bl SetColor
mov r0,#0 @Box for black letters
mov r1,#133
mov r2,#140
mov r3,#153
bl DrawFillBox
Wenn wir den Bildschirm das erste mal anzeigen, bekommen wir einen schwarzen Bildschirm. Für die schwarze Schrift ist das etwas unglücklich, da schwarz auf schwarz sehr deutlich zu lesen ist. Also werden wir an einer Stelle des Screens eine weiße Box zeichnen, auf der später der schwarze String angezeigt wird.
Gleich zu Beginn unseres Programmes verwenden wir unsere Libarie. Wir setzen den Zeiger der Farbe "Color_White" auf r3. Da hier nun der Zeiger auf die Struktur hinterlegt ist, laden wir genau diesen Zeiger nach r3. Nun können wir die Farben, aus dieser Liste nehmen und laden diese in r0 bis r2, damit die Funktion "SetColor" die entsprechenden Farbwerte übermittelt bekommt. Mit "DrawFillBox" zeichnen wir dann die weiße Box.
Damit haben wir unsere Vorbereitungen gemacht.
Für dieses Beispiel werden wir folgenden Pseudocode verwenden.
for a = 0 to 138
{
Load Color(a)
Print String(a),x,y
y=y+20
if y > ScreenY
{
y = 0
x = x + 180
}
}
Zunächst setzen wir einige Ausgangswerte.
mov r6,#0
mov r7,#0
mov r8,#0
ldr r9,=SystemColors @List of Colors
Zunächst legen wir die Position x in r6 und y in r7. In r8 legen wir die entsprechende Farbe und in r9 speichern wir den Zeiger auf die Liste der Farben ab.
Wie der Pseudocode beschreibt, verwenden wir eine "For"-Schleife. "For"-Schleifen scheinen einen einfachen Weg zu beschreiben, wie diese in Assembler umgesetzt werden könnten. Leider stimmt das nicht so ganz. Solche Schleifen werden immer durchgeführt, auch wenn schon beim ersten mal die Bedingungen nicht stimmen. Gleichzeitig müssen wir aber auch die Bedingungen auswerten. In Assembler können wir dies wie folgt umsetzen.
b for_loop
for_loop_intern:
add r6,#20 @ Add y at 20
ldr r10,=scy @Load Adress for Screen (Y)
ldr r10,[r10] @to r10
sub r10,#20 @Sub #20, because nothing can be shown after the end
cmp r6,r10 @Compare with the end
ble next_loop @If not, skip
mov r6,#0 @set y to Null
add r7,#180 @add x at 180 Pixel
next_loop:
add r8,#1 @Add counter by one
for_loop:
ldr r4,[r9] @Get address of the color
add r9,#4 @Increase for next color
ldr r0,[r4] @Load red
add r4,#4 @next
ldr r1,[r4] @Load green
add r4,#4 @next
ldr r2,[r4] @Load blue
bl SetColor @Set color
add r4,#8 @Skip length specification
mov r0,r4 @Load Adress from String
mov r1,r7 @Position x
mov r2,r6 @and Position y
bl DrawString @Show String
cmp r8,#138 @Compare with number of system colors
ble for_loop_intern @Not reached yet
Zunächst überspringen wir unsere Berechnungen mit einem "Branch" und setzen die Farbe und lassen den Farbstring anzeigen. Anschließend vergleichen wir, ob das Ende erreicht wurde. Wenn nicht, kümmern wir uns um unsere Berechnungen, die wir zuvor übersprungen haben.
Den gesammten Sourcecode gibt es hier: DrawColor.zip
| < Zurück (Die Anzeige) | < Hauptseite > |
