Chars (PI5)

Aus C und Assembler mit Raspberry

DrawChar auf dem Raspberry Pi

Wir haben nun eine Pixel-Funktion geschrieben, aber möchten wir nicht auch, dass der Raspberry Pi Informationen direkt auf dem Bildschirm anzeigen kann? Das wollen wir jetzt versuchen. In der Regel kommuniziert der Computer mittels Schriftzeichen. Dies geschieht nicht einfach so; ähnlich wie ein Mensch muss der Computer zuerst lernen, wie jeder einzelne Buchstabe aussieht. Außerdem muss er wissen, wann er das entsprechende Zeichen verwenden soll. Bestimmte Bytewerte stehen für bestimmte Zeichen oder Steuercodes. Es gibt einen Standard, der diese Kommunikation festlegt. Dieser Code nennt sich "ANSI-Zeichencode".

Der ANSI-Zeichencode

Der ANSI-Zeichencode, den wir hier verwenden, ist eine Erweiterung des ASCII-Codes, bei dem von 7-Bit pro Zeichen auf 8-Bit pro Zeichen umgestellt wurde, was einem Byte entspricht. Der ANSI-Zeichencode wurde eingeführt, als Computeranwender über Netzwerke kommunizieren wollten. Dafür war es notwendig, dass beide Computer mit dem gleichen Code "sprechen".

Innerhalb des ASCII-Codes sind die Zahlenwerte für die Zeichen in der Regel identisch, aber in der Erweiterung zu 8-Bit nicht unbedingt gleich. Jede Computerfirma hat eigene Codepages entwickelt und veröffentlicht. Gerade in der Anfangszeit dominierten IBM-Rechner mit dem Betriebssystem MS-DOS. Wir werden hier genau diesen Zeichencode verwenden, der dort eingesetzt wurde und auch unter Windows in der Konsole verwendet wird. Dieser Code wird als "Codepage 437" bezeichnet.

00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0x0 NULL
0x1 §
0x2 Space ! " # $ % & ' ( ) * + , - . /
0x3 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
0x4 @ A B C D E F G H I J K L M N O
0x5 P Q R S T U V W X Y Z [ \ ] ^ _
0x6 ` a b c d e f g h i j k l m n o
0x7 p q r s t u v w x y z { } ~ DEL
0x8 Ç ü é â ä à å ç ê ë è ï î ì Ä Å
0x9 É æ Æ ô ö ò û ù ÿ Ö Ü ¢ £ ¥ ƒ
0xA á í ó ú ñ Ñ ª º ¿ ¬ ½ ¼ ¡ « »
0xB
0xC
0xD
0xE α ß Γ π Σ σ µ τ Φ Θ Ω δ φ ε
0xF ± ÷ ° · ²    

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. Zur Einfachheit verwenden wir eine 8x8 Matrix, um die Zeichen darzustellen.

Dazu erstellen wir eine Datei, die folgendermaßen aussieht:

  • Dimension
    • INT X
    • INT Y
  • Data
    • Binärcode

Zunächst geben wir die Größe der Zeichen in X und Y an. Daraus können wir zur Berechnung der einzelnen Zeichen den entsprechenden Binärcode ableiten. 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" 
.byte 0b11111100
.byte 0b01100110
.byte 0b01100110
.byte 0b01111100
.byte 0b01100110
.byte 0b01100110
.byte 0b11111100
.byte 0b00000000

...

Dieses Beispiel zeigt, wie wir die Datenbank für die Zeichendarstellung aufbauen können. Jeder Buchstabe wird als 8x8 Pixel großes Muster beschrieben, wobei jedes Byte eine Zeile des Musters darstellt. Das Bitmuster gibt an, welche Pixel gesetzt werden und somit sichtbar sind.

Mein Beispiel für den Font habe ich als "ms8x8font.fon" im Includeverzeichnis abgelegt.

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.

//void DrawChar(char c, u32 x0, u32 y0)
//Zeichnet einen Char an die Position x0, y0 auf den Screen
.globl DrawChar
DrawChar:
	stp x29, x30, [sp, -16]!
	stp x20, x21, [sp, -16]!
	stp x10, x11, [sp, -16]!
	stp x12, x13, [sp, -16]!
	stp x14, x15, [sp, -16]!
	mov x29, sp
	
    Sichere zunächst die Position
	mov w20,w1	// x0
	mov w21,w2	// y0

Unseren Font haben wir im Datenbereich abgelegt und ihm das Label "font" gegeben. Diesen Zeiger legen wir in x10 ab.

    //Berechne den Zeiger auf das richtige Zeichen 
	ldr x10,=font
	lsl w0,w0,#3
	add x10,x10,x0

Da wir die Dimension des Fonts kennen, überspringen wir diese Angabe. Danach multiplizieren wir die Übergabe des Zeichen, welche wir in w0 ü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.

Da wir wissen, wie der Font aufgebaut ist, also 8x8 Pixel, verwenden wir zwei ineinander verschachtelte Schleifen:

    //for (w11 = 0; w11 < FONT_HEIGHT; w11++)
	mov w11,#0
1:
	cmp w11,#FONT_HEIGHT
	bge 2f

    //for (w12 = 0; w12 < FONT_WIDTH; w12++)
	mov w12,#0
3:
	cmp w12,#FONT_WIDTH
	bge 4f

...

    //end_for w12
	add w12,w12,#1
	b 3b
4:
    //end_for w11
	add w11,w11,#1
	b 1b

Nun müssen wir jedes Bit dieser Font-Matrix auswerten. Dazu verwenden wir folgende Formel: (Zeile >> (7 - BitPos)) & 1;

Wir lesen zunächst das Byte heraus, was wir mit w11 (Zeile) definiert haben. Dies machen wir mit dem Befehl ldrb w13,[x10,x11]. Dies bedeutet das wir aus dem Speicher das Byte lesen, welches im Speicher an x10 steht und addieren dort den Offset von x11 dazu. Den Wert schreiben wir nach w13. Da wir das Ergebnis falsch herum bekommen, ziehen wir von der 7 (1. Bit = 0) die BitPos ab, die wir in der Schleife mit w12 definiert haben. Verschieben damit dann die Bits im Byte um die Positionen mit lsr nach rechts und mit "and" löschen wir alle höherwertigen Bits, bis auf das erste. Nun können wir das Ergebnis auswerten. Wenn der Wert gleich Null ist, dann zeichne ein Pixel in der Hintergrundfarbe, ansonsten in der Vordergrundfarbe.

Hier der gesamte Code für die DrawChar-Funktion:

//void DrawChar(char c, u32 x0, u32 y0)
//Zeichnet einen Char an die Position x0, y0 auf den Screen
.globl DrawChar
DrawChar:
	stp x29, x30, [sp, -16]!
	stp x20, x21, [sp, -16]!
	stp x10, x11, [sp, -16]!
	stp x12, x13, [sp, -16]!
	stp x14, x15, [sp, -16]!
	mov x29, sp
	
    Sichere zunächst die Position
	mov w20,w1	// x0
	mov w21,w2	// y0

    //Berechne den Zeiger auf das richtige Zeichen 
	ldr x10,=font
	lsl w0,w0,#3
	add x10,x10,x0

    //for (w11 = 0; w11 < FONT_HEIGHT; w11++)
	mov w11,#0
1:
	cmp w11,#FONT_HEIGHT
	bge 2f

    //for (w12 = 0; w12 < FONT_WIDTH; w12++)
	mov w12,#0
3:
	cmp w12,#FONT_WIDTH
	bge 4f
	

    //Überprüfe das Bit nach folgender Formel:
    //(Zeile >> (7 - BitPos)) & 1;
	ldrb w13,[x10,x11]
	mov w14,#7
	sub w14,w14,w12
	lsr w13,w13,w14
	and w13,w13,#1
    //Wenn Ergebniss 0 ist, dann Hintergrund zeichnen
	cbz w13,5f

    //Ansonsten in der Vordergrundfarbe
	ldr x13,=FrontColor
	ldr w13,[x13]
	ldr x14,=DrawColor
	str w13,[x14]
	b 6f
	
5:
	ldr x13,=BackColor
	ldr w13,[x13]
	ldr x14,=DrawColor
	str w13,[x14]

6:
    //Zeichne den Pixel an die Position
	mov w0,w12
	add w0,w0,w20
	mov w1,w11
	add w1,w1,w21
	bl DrawPixel

    //end_for w12
	add w12,w12,#1
	b 3b
4:
    //end_for w11
	add w11,w11,#1
	b 1b
2:
	ldp x14, x15, [sp], 16
	ldp x12, x13, [sp], 16
	ldp x10, x11, [sp], 16
	ldp x20, x21, [sp], 16
	ldp x29, x30, [sp], 16
	ret

Um das nun zu testen, ob es geht, müssen wir unser main-Programm ändern:

//
// kernel.S
//

.section .text
.globl main
main:

    bl LED_off
	bl Init_Screen

	mov w10,#33
	mov w11,#100
2:
	cmp w10,#90
	bge 1f
	
	mov w0,w10
	mov w1,#100
	mov w2,w11
	bl DrawChar
	add w11,w11,#10
	add w10,w10,#1

	b 2b
1:	 
	mov w0,#2
	bl LED_Error
999:
   b 999b

Wie zuvor schon, schalten wir wieder unsere LED aus, um einen Ausgangszustand zu haben. Initialiesieren unseren Screen und erzeugen dann eine Schleife, die von 33 bis 90 zählt. Diesen Wert übergeben wir der DrawChar-Funktion in w0. Zusätzlich geben wir ihr die Koordinaten für den "Char" in w1 (immer 100) und w2 (jeweils um 10 erhöht) mit. Am ende unseres Programms gehen wir wieder in den Fehlercode 2, damit wir sehen, dass er alles abgearbeitet hat.