Tobias Kaiser
A Computer Engineer's Projects and Ideas
Zurück zur Homepage
Inhaltsverzeichnis:

Mit Mikroprozessoren kann man viel Spaß haben. Hier beschreibe ich, wie man selber mit Mikroprozessoren basteln kann.

Erste Z80-Versuche

Da Mikrocomputer irgendwie ja interessant sind, habe ich entschiedenen, einen aus CPU, RAM, ROM etc. selbst zusammenzubauen. Das schöne an alten Mikroprozessoren wie dem Z80 ist, dass sie in handlichen Gehäusen verfügbar sind. Man kann ohne aufwendiges Platinendesign direkt auf einer Lochrasterplatine mit dem Basteln loslegen.

Ich habe eine recht ausführliche Anleitung / Beschreibung eines Z80-Einplatinencomputer gefunden: Einplatinencomputer von Rudi's Homepage. Bei den verwendeten Komponenten habe ich mich an Rudi's Schaltplan orientiert. Ich habe allerdings als CPU die Z84C01-Z80-CPU im PLCC-Gehäuse eingebaut. Als ROM kommt das 128KB-Flash-EEPROM AM29F010B ebenfalls im PLCC-Gehäuse von AMD zum Einsatz. Der UART (TL16C550) ist ebenfalls ein PLCC-Chip.

So sieht das Ergebnis aus:

Z80

Wie man sieht, kann man mit den PLCC-Gehäusen ein Z80-System sehr platzsparend realisieren. Die Kupferlackdraht-Verkabelung auf der Rückseite kann dann allerdings recht unübersichtlich werden:

Z80

Den Kupferlackdraht habe ich leider nicht heiß genug gelötet, damit die Kontaktstellen zuverlässig funktionieren. Daher war es sehr schwer, zuerst die Fehlerstellen in der Verkabelung zu finden. Nach viel Such- und Reparaturarbeit (die Kupferlackdrähte brechen auch gerne mal ab) hat das Z80-System dann schließlich das Monitorprogramm von Rudi's Homepage ausgeführt und ich war sehr glücklich.

Leider funktioniert dieses Z80-System wegen der Verkabelung nicht zuverlässig.

Was lerne ich aus dem Z80-Projekt?

8085-Projekt

Ich habe mir den Spaß erlaubt, mit einem 8085-Prozessor auf einem Elektronik-Steckbrett (Breadboard) einen simplen Mikrocomputer bestehend aus CPU, RAM, ROM und E/A-Komponenten aufzubauen.

Der 8085 ist ein hervorragender 8-Bit-Mikroprozessor, den Intel im Jahr 1977 herausgebracht hat. Obwohl er schon längst veraltet ist, kann man auch für zeitgenössische Prozesoren viel mit dem 8085 lernen. Auf seinen Aufbau und seine Funktionsweise möchte ich hier nicht eingehen, dazu gibt es einmal das Datenblatt und im Weiteren viel Dokumentation im Internet.

Komponenten

Eingabe / Ausgabe

Schaltplan

8085

Für die ICs fehlt im Schaltplan noch je ein 100 nF-Abblockkondensator zwischen Masse und Versorgungsspannung.

Steckbrett

8085

Programme

Assemblerprogramme für den 8085 lassen sich plattformunabhängig mit dem Assembler AS in Binärcode, den der Prozessor versteht, umwandeln.

Monitorprogramm fürs ROM

Ich habe ein Monitorprogramm geschrieben, das über die serielle Schnittstelle völlige Kontrolle über den Computer bietet (Programm laden, Speicher angucken, ...):

Monitorprogramm: Quellcode & Binärdatei fürs ROM

Hilfsprogramm zum Erzeugen einer Eingabe für den Copy&Paste-Lader:

#include<stdio.h>

int main(int argc, char *argv[]) {
    if(argc!=2) {
        printf("%s -- Converts bin to loadable A+length+content hex"
        "\nUsage: %s FILE\n\n",
            argv[0], argv[0]);
        return 1;
    }
    printf("A");
    FILE *f=fopen(argv[1], "r");
    fseek(f, 0, SEEK_END);
    printf("%04X", ftell(f));
    fseek(f, 0, SEEK_SET);
    int c;
    while((c=fgetc(f))!=EOF) {
        printf("%02X", c);
    }
    //printf("\n");
    fclose(f);
    return 0;
}

Beispielprogramme

Folgende Definitionen sind für die Programme nötig. Der Binärcode kann dann vom Monitorprogramm ins RAM geladen werden und wird dann ausgeführt:

IO_DIP_SWITCHES     EQU 0C0H
IO_LED_BAR          EQU 0E0H

; 16550 UART
IO_UART_RBR         EQU 40H ;Receive Buffer Register
IO_UART_THR         EQU 40H ;Transmitter Holding Register
IO_UART_MCR         EQU 44H ;MODEM Control Register
IO_UART_LSR         EQU 45H ;Line Status Register
IO_UART_LSR_THRE    EQU (1<<5)

ORG 8000h
    ; Hier fehlt noch ein Programm

Programm 1

Gibt die Zustände der Schalter mit den LEDs aus.

loop:
    IN IO_DIP_SWITCHES
    OUT IO_LED_BAR
    JMP loop

Programm 2

Ein LED-Lauflicht:

MVI A, 1
loop:
    CALL delay_loop
    RRC
    OUT IO_LED_BAR
    JMP loop

delay_loop:
    DCR C
    JNZ delay_loop ; Innere Schleife
    DCR B
    JNZ delay_loop ; Aussere Schleife
    RET
loop:
    CALL delay_loop
    RRC
    OUT IO_LED_BAR
    JMP loop

Programm 3

Zählt binär mit den LEDs aufwärts. Die Geschwindigkeit lässt sich über die DIP-Schalter einstellen.

MVI A, 0
loop:
    INR A
    OUT IO_LED_BAR
    CALL delay
    JMP loop

delay:
    PUSH B ; Lege B und C auf den Stapel
    PUSH PSW ; Lege A und Flags auf den Stapel
    IN IO_DIP_SWITCHES
    MOV B, A
    MVI C, 00h
delay_loop:
    NOP
    NOP
    NOP
    DCR C
    JNZ delay_loop ; Innere Schleife
    DCR B
    JNZ delay_loop ; Aussere Schleife
    POP PSW ; A und Flags wiederherstellen
    POP B ; B und C wiederherstellen
    RET

Programm 4

Ein etwas kreativeres Lauflicht, das seinen Zustand zusätzlich in hexadezimaler Form über RS-232 ausgibt:

MVI A, 1B
top:
    MVI E, 25
    CALL rot1
    CALL rot1_back
    JMP top

rot1:
    MVI A, 1111B
    MVI D, 0
    MVI E, 20
rot1_top:
    RLC
    CALL common_routine
    RZ
    JMP rot1_top

rot1_back:
    MVI A, 11110000B
    MVI D, 0
    MVI E, 20
rot1_back_top:
    RRC
    CALL common_routine
    RZ
    JMP rot1_back_top

delay:
    PUSH B
    LXI B, 0C000H
delay_loop:
    INR C
    JNZ delay_loop
    INR B
    JNZ delay_loop
    POP B
    RET

write_char:
    PUSH PSW
write_char_busy_loop:
    IN IO_UART_LSR
    ANI IO_UART_LSR_THRE
    JZ write_char_busy_loop
    POP PSW
    OUT IO_UART_THR
    RET

write_hex:
    PUSH PSW
    PUSH B
    MOV B, A
    RLC
    RLC
    RLC
    RLC
    CALL write_hex_nibble
    CALL write_char
    MOV A, B
    CALL write_hex_nibble
    CALL write_char
    POP B
    POP PSW
    RET

write_hex_nibble:
    ANI 1111B

    CPI 10
    JM write_hex_num
write_hex_alpha:
    SBI 10
    ADI 'A'
    RET
write_hex_num:
    ADI '0'
    RET

common_routine:
    CALL delay
    CALL write_hex
    OUT IO_LED_BAR
    PUSH PSW
    MVI A, ' '
    CALL write_char
    POP PSW

    PUSH B
    INR D
    MOV B, A
    MOV A, D
    CMP E
    JNZ not_end

    ; set zero flag:
    MVI A, 0
    CPI 0

    MOV A, B
    POP B
    RET
not_end:
    ; reset zero flag:
    MVI A, 0
    CPI 1

    MOV A, B
    POP B
    RET

Programm 5

Spiel: Das Bit mit den DIP-Schaltern einsperren:

MVI A, 1B
top:
    CALL rot2
    CALL rot2_back
    JMP top

rot2_back:
    MVI D, 0
rot2_back_top:
    RRC
    MOV B, A
    IN IO_DIP_SWITCHES
    ANA B
    RNZ
    MOV A, B
    CALL common_routine
    JMP rot2_back_top

rot2:
    MVI D, 0
rot2_top:
    RLC
    MOV B, A
    IN IO_DIP_SWITCHES
    ANA B
    RNZ
    MOV A, B
    CALL common_routine
    JMP rot2_top

delay:
    PUSH B
    LXI B, 0C000H
delay_loop:
    INR C
    JNZ delay_loop
    INR B
    JNZ delay_loop
    POP B
    RET

common_routine:
    CALL delay
    OUT IO_LED_BAR

    PUSH B
    INR D
    MOV B, A
    MOV A, D
    CMP E
    JNZ not_end

    ; set zero flag:
    MVI A, 0
    CPI 0

    MOV A, B
    POP B
    RET
not_end:
    ; reset zero flag:
    MVI A, 0
    CPI 1

    MOV A, B
    POP B
    RET

Programm 6

Liest Zeichen über RS-232 ein und gibt sie auf den LEDs aus:

loop:
    CALL read_char
    CPI '\r'
    JZ end

    CALL write_char
    OUT IO_LED_BAR
    JMP loop
end:
    RST 1

write_char:
    PUSH PSW
write_char_busy_loop:

    IN IO_UART_LSR
    ANI IO_UART_LSR_THRE
    JZ write_char_busy_loop
    POP PSW
    OUT IO_UART_THR
    RET

read_char:
    DI ; Disable Interrupts
read_char_busy_loop:
    IN IO_UART_LSR
    ANI IO_UART_LSR_DR
    JZ read_char_busy_loop
    IN IO_UART_RBR
    DI ; Reenable Interrupts
    RET

Programm 7

Dieses Beispielprogramm setzt die Register und gibt dann den Zustand des Prozessors aus.

MVI A, 12H
    CPI 10H
    LXI B, 3456H
    LXI D, 789AH
    LXI H, 0BCDEH
    CALL print_status
    RST 1

write_char:
    PUSH PSW
write_char_busy_loop:
    IN IO_UART_LSR
    ANI IO_UART_LSR_THRE
    JZ write_char_busy_loop
    POP PSW
    OUT IO_UART_THR
    RET

write_string:
    PUSH PSW
    PUSH H
write_string_loop:
    MOV A, M
    CPI 0
    JZ write_string_end
    CALL write_char
    INX H
    JMP write_string_loop
write_string_end:

    POP H
    POP PSW
    RET

write_hex:
    PUSH PSW
    PUSH B
    MOV B, A
    RLC
    RLC
    RLC
    RLC
    CALL write_hex_nibble
    CALL write_char
    MOV A, B
    CALL write_hex_nibble
    CALL write_char
    POP B
    POP PSW
    RET

write_hex_nibble:
    ANI 1111B

    CPI 10
    JM write_hex_num
write_hex_alpha:
    SBI 10
    ADI 'A'
    RET
write_hex_num:
    ADI '0'
    RET

a_message: DB "\n\rA:  ", 00H
flags_message: DB " Flags: ", 00H
bc_message: DB "\n\rBC: ", 00H
de_message: DB "\n\rDE: ", 00H
hl_message: DB "\n\rHL: ", 00H
sp_message: DB "\n\rSP: ", 00H

print_status:
    PUSH PSW

    PUSH H
    LXI H, a_message
    CALL write_string
    POP H
    CALL write_hex

    PUSH H
    LXI H, flags_message
    CALL write_string
    POP H

    MVI A, 'Z'
    CALL write_char
    MVI A, '='
    CALL write_char
    MVI A, '0'
    JNZ after_iszero
    MVI A, '1'
after_iszero:
    CALL write_char

    MVI A, ' '
    CALL write_char

    MVI A, 'S'
    CALL write_char
    MVI A, '='
    CALL write_char
    MVI A, '0'
    JP after_issigned
    MVI A, '1'
after_issigned:
    CALL write_char

    MVI A, ' '
    CALL write_char

    MVI A, 'C'
    CALL write_char
    MVI A, '='
    CALL write_char
    MVI A, '0'
    JNC after_iscarry
    MVI A, '1'
after_iscarry:
    CALL write_char

    MVI A, ' '
    CALL write_char

    MVI A, 'P'
    CALL write_char
    MVI A, '='
    CALL write_char
    MVI A, '0'
    JPO after_isparity
    MVI A, '1'
after_isparity:
    CALL write_char

    MVI A, ' '
    CALL write_char

    PUSH H
    LXI H, bc_message
    CALL write_string
    POP H
    MOV A, B
    CALL write_hex
    MVI A, ' '
    CALL write_char
    MOV A, C
    CALL write_hex

    PUSH H
    LXI H, de_message
    CALL write_string
    POP H
    MOV A, D
    CALL write_hex
    MVI A, ' '
    CALL write_char
    MOV A, E
    CALL write_hex

    PUSH H
    LXI H, hl_message
    CALL write_string
    POP H
    MOV A, H
    CALL write_hex
    MVI A, ' '
    CALL write_char
    MOV A, L
    CALL write_hex

    PUSH H
    LXI H, sp_message
    CALL write_string
    POP H
    PUSH D

    ; undokumentierter Befehl des 8085, der den Inhalt des Stapelzeigers
    ; addiert mit einem Wert im Registerpaar HL speichert.
    DB 38H, 00H
    
    ; Substract what this function adds on stack
    INX D
    INX D
    INX D
    INX D
    INX D
    INX D
    MOV A, D
    CALL write_hex
    MVI A, ' '
    CALL write_char
    MOV A, E
    CALL write_hex
    POP D

    POP PSW
    RET

Mehr zu undokumentierten Befehlen des 8085: http://sangrah.weebly.com/2/post/2011/10/undocumented-8085-flags-and-instructions1.html

Programm 8

Dieses Programm gibt über RS-232 bei jeder Änderung den Zustand der Schalter aus.

loop:
    IN IO_DIP_SWITCHES
    CMP B
    JZ loop
    PUSH PSW
    CALL write_hex
    MOV B, A
    MVI A, ' '
    CALL write_char
    POP PSW
    JMP loop

write_char:
    PUSH PSW
write_char_busy_loop:

    IN IO_UART_LSR
    ANI IO_UART_LSR_THRE
    JZ write_char_busy_loop
    POP PSW
    OUT IO_UART_THR
    RET

write_hex:
    PUSH PSW
    PUSH B
    MOV B, A
    RLC
    RLC
    RLC
    RLC
    CALL write_hex_nibble
    CALL write_char
    MOV A, B
    CALL write_hex_nibble
    CALL write_char
    POP B
    POP PSW
    RET

write_hex_nibble:
    ANI 1111B

    CPI 10
    JM write_hex_num
write_hex_alpha:
    SBI 10
    ADI 'A'
    RET
write_hex_num:
    ADI '0'
    RET

8088-Computer

Nachdem der Z80-Computer weniger gut funktioniert hat, der 8085-Computer dann auf dem Steckbrett aber sehr gut, hatte ich Lust, mal einen etwas fortschrittlicheren Prozessor zu benutzen. Der 8088 ist intern nämlich ein 16-Bit-Rechner. Der externe Datenbus ist 8 Bit breit. Durch Speichersegmentierung ist derAdressbus 20 Bit breit. Somit kann man bis zu 1 MB Speicher direkt an den Prozessor anschließen, dies ist verglichen zu 64 KB Speicher beim Z80, 8085 oder AVR eine Menge.

Nach intensivem Studium der Datenblätter und ein bisschen Planung, begann ich dann, einen 8088-Computer auf dem Steckbrett mit dem 8085-Aufbau als Vorbild im Hinterkopf aufzubauen. (Der 8088 ist dafür ausgelegt, sich in Systemen gut als Nachfolger des 8085 zu eignen. Allerdings sind die Prozessoren keinesfalls pinkompatibel oder binärkompatibel.) Zuerst funktionierte der 8088 auf dem Steckbrett auch wunderbar. Irgendwann aber nicht mehr. Ich weiß bis heute nicht, was da der Fehler war. Vielleicht ein schlechter Kontakt oder ein unsichtbarer Kurzschluss im Steckbrett. Ich hatte auch nicht mehr die Lust auf elendig lange Fehlersuche mit dem Multimeter im Steckbrett-Kabelsalat (20-Bit-Busse sind meiner Meinung nach zu breit für Steckbretter), also übertrug ich den Aufbau auf eine Lochrasterplatine:

8088

Einzelteile:

Da ich hier die Verkabelung mit Schaltdraht auf der Bestückungsseite vorgenommen habe, funktioniert der Computer zuverlässig.

Wie immer habe ich ein kleines Monitor- bzw. Ladeprogramm für den Rechner geschrieben.

Das wirklich interessante am 8088 ist, dass er als 100% binärkompatibeler, nur langsamerer, kleiner Bruder des 8086 direkter Vorgänger der heute in Dekstop-PCs und Notebooks eingesetzten x86-CPUs ist. Die heute verbreiteten CPUs sind lediglich Erweiterungen des ursprünglichen 8086/8088. Im Real Mode arbeiten auch heutige Prozessoren genau so, wie der original 8086/8088.

Sehr interessant am 8086/8088 ist, dass durch seine Popularität und die Popularität seiner Nachfolger viel Software entstanden ist. So hat man für Assemblerprogrammierung die Wahl zwischen vielen populären und gebräuchlichen Assemblern, wie z. B. dem Netwide Assembler, dem GNU Assembler oder dem Microsoft Macro Assembler.

Mit den C-Compilern sieht es leider schon etwas dünner aus. Auf die Schnelle hat Bruce's C Compiler gut funktioniert. Mit der Option -ansi wird er zwar brauchbar, leider ist er vom Komfort nicht vergleichbar mit den großen Kompilern. In Frage kommt noch der Watcom-Kompiler oder eventuell sogar der GNU C Compiler, da müsste man aber noch ein bisschen friemeln, damit diese Kompiler 8086/8088-kompatibelen 16-Bit-Code erzeugen.

Wenn man zusätzlichen Hintergrundspeicher, z. B. eine SD-Karte, anschließt, wäre es auch denkbar, „richtige“ Betriebssysteme auf diesem Rechner laufen zu lassen. Denkbar wären da:

Kommentare

comments powered by Disqus