Home | Artikel | VCFe SoftVGA Interna

VCFe Vortrag vom 2019.04.27 - Programmieren am Limit - SoftVGA Interna


Inhalt

Einführung

Gerade passend zum diesjährigen Thema "Programmieren am Limit" erreicht mein SoftVGA Projekt in 2019 das Alter von 10 Jahren. Der erste VCFe Vortrag zu dieser, als Beispiel verwendet im Vortrag Softe Hardware - Emulation von hochintegrierten Chips auf Mikrocontrollern, war sogar am 2009.05.01, und damit genau am VCFe vor 10 Jahren! Also Zeit, die SoftVGA ihre Interna zu beschrieben, insbesondere die Evolution der Zeichenalgorithmen und Datenstrukturen durchzugehen, sowie ihrer darstellerischen Möglichkeiten anzusschauen. Zumal all diese Algorithmen genau am Rande des Programmierbaren liegen. Mit dazu noch als Kontrast einen Vergleich mit dem auf einem neueren grösseren AVR Chip basierten NTSC/SCART Videogenerator der Uzebox/EUzebox Spielkonsole. Sowie einen Vergleich mit dem auf der in TTL implementierten Gigatron Harward Architektur Microengine laufenden VGA Generator.

Vorgeschichte und Kontrast

In Juli 2004 begegnete ich beim Websurfen einem Projekt von Rickard Gunee, dem PIC Game System, welches auf der Basis eines nicht gerade leistungsfähigen PIC16C84 Mikrocontrollers mit nur 12MHz/3MIPS einen primitiven Videogenerator für Schwarzweiss NTSC Fernsehen implementierte, der ausreichte um Pong zu spielen, wozu etwa 40 Pixel pro Zeile benötigt werden.

Ein weiteres Projekt welches ich Nov 2004 fand, das PIC Pong von Jaakko Hyvätti, schaffte selbiges sogar auf einem kleineren PIC12F675 Mikrocontroller, aber diesen mit immerhin 16MHz/4MIPS leicht schneller, dafür aber auch mit Paddles statt nur Joystick.

Bei einem weiteren Projekt welches ich Mai 2005 fand, hatte Robert Greene es bis zu 64x44 Pixel auf einem VGA Monitor geschafft, mit Farben, auf einem etwas besseren PIC18LF252. Limite war bei diesem wie langsam der Controller die Pixel hinausschieben konnte, aber auch wie wenig Bildspeicher ins SRAM der Mikrocontroller passte.

Mein Interesse war damit geweckt. Aber meine Anforderungen waren weit mehr. Ich wollte volle 40x24 (Apple II, Atari 800) Zeichen Anzeige, oder besser gar 40x25 (PET, C64, CGA), oder minimal noch 32x24 (TMS9918, MSX), weil bereits eine 32x16 (MC6847) Anzeige mühsam wenig ist. Dies zudem mit mindestens 4x8 Pixel Font, und damit 160x192 bzw 160x200 Pixel, in mindestens den 8 Primärfarben. Dies sah nach nicht machbar aus, weil die diversen Controller zumeist weitaus zu langsam für das schnelle VGA Timing waren. (Am ehersten würde noch eine ZX80/81-artige Technik mit Z80 drinliegen, aber das wäre nur 1bpp Schwarzweiss, und damit die VGA sinnlos. Ausser man addiert Attribute dazu, was ich damals nicht in Betracht zog.)

VGA wiederum ist nötig für dessen einfaches direktes RGB Signalisierungsverfahren. Das NTSC/PAL Fernsehen QAM Verfahren tue ich mir definitiv nicht an, weil das ist sehr kompliziert, bei PAL sogar noch schlimmer als bei NTSC, und so noch viel mehr Rechenleistung braucht, trotz eigentlich langsameren Signalen. (Die Existenz von RGB zu NTSC/PAL Analogcodierchips hatte ich damals vermutet. Aber solche waren mir keine bekannt, und ich hab nicht nach solchen gesucht.)

Am VCFe 2006 wurde eine Replica-1 ausgestellt. Diese ist ein Apple 1 Clone, mit den passenden 65C02 + 32k SRAM + 32k EEPROM + 6821, welcher aber anstelle von dessen halber Platine an schwer zu beschaffender Terminal Logik nur 2 AVRs ATmega8515 plus ATmega8 plus 74HC166 Shiftregister und 74HC74 2*FF benutzte, all das nur um ein 240x192 Schwarzweiss NTSC Fernsehsignal zu erzeugen. Diesen hab ich zuhause im Web genauer angeschaut. (Seither habe ich sogar einen AVR ATmega32/328 plus 74165 gefunden, der volle 80x25 mit CGA 8x8 PC437 Font in Schwarzweiss mit PAL Timing macht, aber die Site ist zweite Hälfte Nov 2019 verschwunden.)

Dabei fand ich aber auch einen weiteren vergleichbaren Clone namens A-ONE, der mit einem einzelnen AVR ATmega32 ohne Hilfs-AVR und Shiftregister und FFs auskommt, weit besser. Beide verwenden zudem einen weiteren kleinen AVR um eine PS/2 Tastatur anzuschliessen, im A-ONE ein ATtiny2313, im Replica-1 ein weiterer ATmega8.

Am VCFe 2008 wurde das AVR-ChipBasic gezeigt, das auf nur einem schnellen und grossen AVR ATmega644 30 Zeichen pro Zeile schafft, sogar mit 6x8 Pixel Font und damit 180 Pixel pro Zeile, in 8 Farben, auf einem SCART Fernseher (diese können teils auch RGB Verfahren), und danaben noch für einen Basic Interpreter Platz fand. Statt dem erwartetem Faktor 10 nur noch um Faktor 2..3 daneben. Es muss doch gehen!

Ich habe dann zuhause sofort die notwendigen Taktzyklen genau nachgerechnet: Es sollte eigentlich klappen! Das Resultat wurde mein SoftVGA Projekt, auf der Basis einer AVR ATmega32 der 16MHz/14.4MIPS kann. Diese wird vom Design auf 3/4 der VGA 25.175MHz (= 18.881MHz) etwas übertacktet (+18%), aber dann dies wegen vorhandenen Quarzen auf 18.432MHz untertaktet. Darauf habe ich ein VGA Interface mit 64 Farben implementiert. Dieses Projekt dauerte Mai 2008 bis Juni 2008 (Hardware), sowie von September 2008 bis März 2009 (Software). Es wurde im Softe Hardware Vortrag am VCFe 2009 als Beispiel benutzt. Dabei habe ich mehrere Generationen von Zeichenalgorithmen entwickelt, welche weiter unten beschrieben werden.

Bedingt durch ein anderes grosses nicht-Elektronik/nicht-Computer Projekt musste ich die SoftVGA dann liegenlassen. Nur Ideen welche aufkamen wurden noch notiert. Insbesondere Details ausarbeiten zu wie die SoftVGA zusammen mit einem Tastatur Gegenstück namens SoftPS2 die zwei Hälften eines SoftTERM Terminals ergibt, und dieses zusammen mit einem SoftCPU Prozessor die drei Teile eines SoftHC Homecomputers. Aber man kann auch nur die SoftVGA als Debugausgabe für beliebige Bastelprojekte nutzen, und die SoftPS2 als fakultative Testeingabe dazu. Daher sollten beide mit beliebigen PIO/PIA oder SIO/UART oder SPI Interfaces laufen, und wegen dies mit einem einfachen 8bit Codesatz angesteuert werden, nicht mit den umständlichen 7bit ASCII basierten ANSI Escape Sequenzen. Die ASCII und Kommandozeichen Codenummern für SoftVGA Videoausgabe sind inzwischen recht detailliert designt, sowie ASCII und Statuszeichen Codenummern für SoftPS2 Tastatureingabe.

Auf der Hardwareseite ist seither nur eines passiert. Ein Kollege brauchte Mai 2009 dringend 40pin AVRs, wozu zwei meiner vorrätigen ATmega32 ausreichten. Ich habe als Gegenleistung in Dez 2009 zwei ATmega644 die 20MHz/18MIPS können bekommen, welche zudem 4k statt 2k SRAM haben. Diese erlauben mehr Zeichen im Font dank mehr SRAM, sowie bessere Zeichenalgorithmen wegen mehr Takt, sowie mit diesem plus immer noch genug SRAM mehr Farben pro Zeichen. Zu welchen ich aber wegen liegenlassen lange Zeit nur Spezifikationen notieren konnte. In April 2018 habe ich noch einen weiteren ATmega32 bei einem anderen Kollegen gegen einen ATmega1284 abgetauscht, um bei mir und ihm die Chipauswahl zu verbreitern.

Ab Dezember 2018 konnte ich endlich die Arbeit wieder aufnehmen, mit zuerst den Projektstatus nachtragen, dann diesen Vortrag schreiben mit dabei mich wieder einzuarbeiten, um ab Februar 2019 am Projekt weitermachen. Aber leider hat eine massive Entgleisung bei Webadmins eine Notsituation für Retrocomputer und Retrosoftware Benutzer erzeugt, welche aus immer mehr von dem Internet herausgeworfen werden, inzwischen über 90% davon. Gegen welches ich eine Kampage um den Untergang von HTTP zu reversieren aufgebaut habe. Dies hat am Projekt weitermachen verhindert, weshalb es jetzt erst ab Dezember 2019 hier weiter geht.

Das restliche Internet hat inzwischen nicht geschlafen. Im April 2017 begegnete ich der Uzebox Spielkonsole, und habe sie nach im März 2018 wieder erwähnt sehen im Web nachgeschaut. Diese wurde ebenfalls von obigem PIC Game System inspiriert. Sie fing auch 2008 an. Aber der Designer hatte bereits einen AVR ATmega644 der 20MHz/18MIPS kann, den er zudem auf gnadenlose 2*14.318=28.636MHz (+43%!) übertacktet hat. (Dies mit Angabe, dass etwa 10% der AVR es nicht mehr aushalten, man diesen aber mit Overvolting noch nachhelfen könne!) Zudem hat er ein VGA Interface mit 256 Farben implementiert. Dies aber mit einem AD725 RGB zu NTSC/PAL Analogcodierchip in der Uzebox (der dort aber nur NTSC kann wegen den festen 14.318MHz, PAL bräuchte 17.734MHz), bzw direkt an SCART in der EUzebox (die Version welche mir begegnete). Daher kann er statt dem sehr schnellem VGA Timing das etwas anspruchlosere (nur 5/8 Frequenzen) NTSC Timing benutzen. Dazu kommt aber noch weit mehr an Takt. Was ihm neben mehr Pixel auch mehr Zyklen/Pixel erlaubt. Er hat inzwischen ebenfalls mehrere Generationen bzw Modi von Zeichenalgorithmen entwickelt, welche noch weiter unten beschrieben werden.

Videosignale und VGA

Das grosse Problem bei Video generieren sind die hohen Frequenzen, bei denen genau wiederholte detailliert strukturierte Signale erzeugt werden müssen. Die Struktur von Videosignalen wurde bereits vor einigen Jahrzehnten definiert, als Digitalelektronik noch fast gar nicht existierte, weshalb diese reine Analogsignale sind. Dabei wurde von bereits definierten Radio Audiosignalen ausgegangen, welche schlicht die Schallwelle als identisch varierende elektrische Spannung representieren, weshalb dies eben analog ist. Dabei waren die Audiosignale aber von niederer Frequenz, weil wir nur bis etwa 20kHz höhren.

Für Video musste dieses erweitert werden, weil die Helligkeit neben in der Zeit (was ein Bewegtbild erlaubt) auch noch in der Fläche variert (was überhaupt erst ein Bildmuster ergibt). Daher wird die Fläche in einen Satz von Zeilen zerlegt (NTSC 480, PAL 576), welche von oben nach unten durchgangen werden, jede davon wiederum von links nach rechts durchgegangen. Dabei wird die Helligkeit des aktuellen Ortes/Punktes als varierende Spannung representiert, weshalb dies auch analog ist. Nach allen Zeilen vom Bild wird wieder oben angefangen, das zyklisch wiederholt (NTSC 60 Halbbilder/Sekunde, PAL 50 Halbbilder/Sekunde), weshalb die Zeit und somit Bewegung in Schritten geschieht, was aber wegen schneller als unser Sehvermögen sein unsichtbar bleibt.

Aus diesem Grund gibt es bei Analogvideo auch keine feste horizontale Pixelzahl, weil Analogsignale zu beliebigem Zeitpunkt varieren können. Wenn auch sie bei zu hoher Frequenz für die beteiligten Verstärker anfangen an Spannung zu verlieren, und wegen limitierter Bandbreite der Übertragung auch bewusst limitiert werden (NTSC 4.5MHz, PAL 5.5MHz) um andere Sender nicht zu stören. Erst von einer Taktfrequenz getriebene digitale Videogeneratoren legen eine solche Pixelzahl fest. Ebenso auf solchem aufgebautes Digitalvideo.

Bedingt durch die Konstruktion der damals verwendeten Bildröhren bzw ihrer analoger Ansteuertechnik, muss nach jeder Zeile eine Pause eingelegt werden, während der Abtaststrahl in der Kamera und der Zeichenstrahl im Fernseher/Videomonitor von rechts wieder nach links zurücklaufen. Dies braucht etwa 1/4 der Zeichenzeit, womit eine Zeile (bei NTSC 63.5µs, bei PAL 64µs) aus etwa 4/5 zeichnen (bei NTSC etwa 51µs, bei PAL 52µs) und 1/5 rücklaufen besteht. Diese Zeilenzeit ergibt mit obigen Zeilen/Bild und Halbbildern/Sekunde bei NTSC 15750 Zeilen/Sekunde und bei PAL 15625 Zeilen/Sekunde.

Selbige Rücklaufzeit würde eigentlich auch für die geringere Distanz von unten nach oben ausreichen, aber die analoge Ansteuertechnik verlangt dafür nach einer grösseren Pause, wieder etwa 1/4 von der Halbbild Zeit. Das ergibt bei NTSC mit 15750/60=262.5 Zeilen/Halbbilder und bei PAL mit 15625/50=312.5 Zeilen/Halbbilder, davon nur 480/2=240 bzw 576/2=288 gezeichnet, und der Rest rücklaufend. Ein Vollbild besteht dabei aus 2 Halbbildern, welche um eine halbe Zeilenhöhe verschoben abgetastet bzw gezeichnet werden, wegen dem 0.5 in der Zeilenzahl, was als Interlaced Scan bezeichnet wird. Daher hat ein Vollbild eben 2*240=480 (480i genannt) bzw 2*288=576 (576i) Zeilen, aber es hat nur 60/2=30 bzw 50/2=25 solcher um Bandbreite zu sparen. Dies mit trotzdem 60 bzw 50 Halbbilder zeichnen um flackern zu reduzieren, wenn auch zum Preis dass es wegen dem Halbbilderversatz um die halbe Zeile etwas flimmert.

Während den Pausen wird schwarz übertragen, damit der zurücklaufende Zeichenstrahl das Bild nicht stört. Anfangs wurde versucht, diese Schwarzpausen zu benutzen, um die Strahlposition im Fernseher/Videomonitor mit der Strahlposition in der Kamera zu synchronisieren, was aber bei dunklem Bild zeitlich unstabil wurde und zu wanderndem Bild führte. Also werden in den Pausen für eine Weile "schwärzer als Schwarz" gesendet, was einen Puls ergibt, den die Synchronisation selbst mit einfachen Analogmitteln sicher erkennen kann. Das Videosignal besteht seither aus Bild von 0.3V Schwarz bis 1.0V Weiss und Synchronisieren mit 0.3V Normal-Schwarz zu 0.0V Puls-Schwarz.

Typische 2. Hälfte 1970er und 1. Hälfte 1980er Rechner haben solche normalen Fernseher/Videomonitore als Ausgabe benutzt. Weshalb sie genau obige Signale erzeugen mussten. Dabei wurden, um Aufwand mit 0.5 sowie Flimmern an horizontalen Kanten/Linien zu reduzieren, statt Vollbildern aus 2 verschobenen Halbbildern nur alles identische Halbbilder erzeugt, mit der 0.5 in der Zeilenzahl weglassen, was als Progressiv Scan bezeichnet wird. Das ergab weniger flimmernde 60 bzw 50 Vollbilder, aber mit nur noch noch 240 (240p genannt) bzw 288 (288p) Zeilen. Davon kam auch das typische damalige streifige Aussehen der erzeugten Bilder.

Des weiteren wurden nur etwa 35 (im TMS9918 und MC6847), oder 40 (im Apple II und C64 und CPC), oder 45 (im Atari 800 und IBM CGA) der etwa 51µs bis 52µs an sichtbarer Breite genutzt, was die bildlosen Ränder links und rechts erzeugte. Ebenso nur 192 (im TMS9918 und MC6847 und Apple II und Atari 800), oder 200 (im C64 und CPC und CGA) der Zeilen, was die bildlosen Ränder oben und unten erzeugte. Die damals üblichen 256 oder 280 oder 320 Pixel/Zeile in diese 35 oder 40 oder 45µs hineinpassen verlangte nach Taktfrequenzen von etwa 6bis8MHz. Zuoberst die 320 Pixel in 40micro;s der C64 mit 8MHz. Ausnahme bei den 640 Pixel in 45µs der CGA mit 14.318MHz bzw in 40micro;s der BBC und CPC mit 16MHz. Man beachte noch, dass dieses Pixel Taktfrequenzen sind, aber obige NTSC 4.5MHz und PAL 5.5MHz dagegen Wellenfrequenzen, welche doppelte oberste Taktfrequenzen sind, also bis zu 9MHz bzw 11MHz Pixelfrequenz erlauben. Daher brauchen erst CGA und BBC und CPC ihre RGB Spezialmonitore, trotz eigentlich NTSC bzw PAL Timing.

Für mehr als das mussten spezielle Computermonitore entwickelt werden. Diese zeichnen sich vor allem durch höhere Frequenzen aus. Ausserdem wurden die ohnehin bildlosen Ränder zeichnen und ihr Zeitverbrauch eingespart, um die Frequenzen etwas zu reduzieren. Bei der VGA erlaubt dies im Graphikmodus 60 Bilder/Sekunde und im Textmodus sogar 70 Bilder/Sekunde. Es hat stets das doppelte der NTSC 15750 Zeilen/Sekunde, also 2*15750=31500, womit es trotz Progressiv 31500/60=525 bzw 31500/70=450 Zeilen/Bild hat, mit 480 bzw 400 davon gezeichnet, und der kleine Rest rücklaufend. Die Zeilen sind wegen doppelt so viele halb so lang, mit 63.5/2=31.75µs, wovon wieder 4/5 gezeichnet werden, wegen kleiner Rest rücklaufend, was aber ohne Rand 25.4µs an Zeile ausnutzt. Die 640 Pixel im Graphikmodus verlangen nach 25.175MHz, die 720 Pixel im Textmodus sogar 28.322MHz. In die 720x400 vom Textmodus passen 80x25 Zeichen mit 9x16 Font, in die 640x480 im Graphikmodus sogar 80x30 Zeichen mit 8x16 Font.

Wäre alles nur Schwarzweiss (bzw strikter Graustufen) geblieben, wäre das schon alles gewesen, und die SoftVGA wäre wegen den anspruchloseren 35 bis 45µs statt den kürzeren 25.4µs für Fernseher/Videomonitore designt worden, und hiesse dann auch SoftVID. Siehe obige Apple 1 Clones, welche genau das machen. Aber die Welt ist farbig, und Fernsehen wurde ebenso. Dafür müssen statt nur einer Helligkeit drei Farbanteile Rot Grün und Blau verwendet werden. Diese kann man direkt mit 3 analogen Leitungen übertragen, was als RGB bezeichnet wird. Der VGA Monitor verwendet genau dies, mit diese analog 0.7V, sowie 2 weitere separate digitale TTL Leitungen für die Synchronpulse (= separate sync), statt sie mit den 0.3V ans Bildsignal zu hängen (= sync on green).

Die Farben sind aber bei Fernseher/Videomonitor so zu übertragen, dass die Signale nur eine Leitung brauchen, und auch mit älteren Schwarzweissfernsehern kompatibel blieben, ebenso ist die nun dreifache Signalmenge etwas zu komprimieren mit die Farben aber trotzdem zu erhalten. Dies erforderte mit damaliger Analogtechnik einige Klimmzüge. Dabei wird das bestehende Helligkeitssignal nun als Y = R+G+B analog errechnet, sowie nur noch zwei Farbdifferenzen U = B-Y und V = R-Y dazu geliefert. Schwarzweissfernseher benutzen nur Y und ignorieren U und V, neuere filtern diese sogar geziehlt heraus. Farbfernseher rechnen analog zurück mit B=U+Y R=V+Y und G=Y-R-B. Dazu werden noch die beiden U und V auf geringere Frequenz reduziert, auf etwa 1/3.

Dann müssen aber trotzdem alle drei gemischt übertragen und im Fernseher/Videomonitor wieder aufgeteilt werden. Das ist der schwierige Teil. Das dafür verwendete QAM Verfahren ist auf Analogtechnik ausgelegt, und daher mit der in der SoftVGA verwendeten Digitaltechnik nicht oder zumindest nur schwierig machbar. Also nichts mit farbigem SoftVID, und nur Schwarzweiss bringts einfach nicht. Daher wird hier ein VGA Monitor benutzt, weil mit RGB Signalen ansteuerbar, trotz dies zeitlich schwieriger sein. Dazu habe ich allerdings dessen knappe Zeiten reduziert, mit den niedereren Graphik 640 Pixel als Basis nehmen, und dann 40 statt 80 Zeichen sowie 4 statt 8 Pixel Breite erzeugen, für 640/2/2=160 Pixel, und somit noch 25.175/2/2=6.294MHz Taktfrequenz.

Man beachte noch, dass manche 1980er Computermonitore normales NTSC/PAL Timing mit RGB Signalen kombiniert hatten, wie die BBC und CPC und Atari ST, aber diese sind heute selten und ausser Produktion. Die CGA hatte NTSC Timing mit IRGB kombiniert, mit neben H und V auch die Farben als 4bit digital übermitteln und erst im Monitor zu analog machen, aber diese sind heute ebenso selten und ausser Produktion. VGA ist der einzige weit verbreitete RGB Computermonitor, und wird bis heute hergestellt. (Strikte wäre ein SoftVID auch mit einem SCART Fernseher machbar gewesen, falls dieser die RGB Signale kann, wie im AVR-ChipBasic und in der EUzebox benutzt. Aber ich hatte damals keinen solchen, und wusste bis zum AVR-ChipBasic sehen auch nicht dass SCART auch RGB beinhaltet, und damals war bereits der Entscheid zu VGA gegeben.) (Ebenso wäre es mit einem RGB zu NTSC/PAL Codierchip machbar gewesen, wie in der Uzebox mit dem AD725 auch benutzt. Aber solche waren mir damals in 2004..2009 keine bekannt, wenn auch ich deren Existenz als wahrscheinlich angenommen hatte, aber nicht danach gesucht.)

SoftVGA Hardware

Die AVR Mikrocontroller sind, wie bei allen Chips dieser Art, kleine vollständige Computersysteme auf einem Chip. Neben einem eigenen 8bit Prozessor (mitsammt Reset- und Taktlogik), sowie SRAM für Daten und Flash für Programme (mitsammt Programmiereinrichtung), haben diese vor allen eine Auswahl an diversen kleineren Ein-/Ausgabeschaltungen darin. Neben einer Handvoll Pins für Stromversorgung und Resettaster und Taktquarz (bei den hier relevanten ATmega32 und ATmega644 sind das 8 Pins von 40), sind die meisten Pins mit den Ein-/Ausgabeschaltungen verbunden (hier die restlichen 32). Dabei sind sie alle zumindest als generisches Parallel Input/Output (PIO) verwendbar. Wegen 8bit sind sie dabei in 8er Gruppen ansprechbar, welche Ports genannt werden, mit 32/8=4 davon, als PA bis PD bezeichnet, die Pins als PA0..7 bis PD0..7.

Hängt man solche Controller Pins direkt an die RGB Anschlüsse eines VGA Steckers würde wegen deren 5V Signalen der 0.0V bis 0.7V Farbsignal Bereich des VGA Monitors massiv überschritten werden. Daher muss man die Spannung mit Analogelektronik herunterteilen. Da die VGA ihre Eingänge wegen MHz über ein Kabel schicken bereits mit 75ohm Widerständen terminiert sind, kann man einfach einen (75/0.7)*5-75 = 460ohm Bremswiderstand zwischen AVR Pin und VGA Stecker Pin legen. Dies für die 3 RGB Pins des VGA Steckers dreimal separat gemacht, erlaubt mit diesen ihren 3 Bits bereits die 2*2*2=8 Primärfarben zu erzeugen, wie es das AVR-ChipBasic machte.

Dies nutzt aber nur 3 Controller Pins aus. Besser ist mit mehr Pins mehr Farben zu erzeugen. Mit 3*2=6 Pins kann man jede der 3 RGB Pins des VGA Steckers mit 2 davon verbinden, um 4*4*4=64 Farben zu bekommen. Dabei braucht es aber auch 6 Bremswiderstände. Diese rechnet man so, dass jeweils Paare 0/3 1/2 2/3 und 3/3 der 0.7V erzeugen, also pro Paar einer 1/3*0.7=0.233V und der andere 2/3*0.7=0.466V, zusammen dann 3/3*0.7V, was dann 1534ohm bzw 730ohm braucht (was wegen den real hergestellten Bauelementen bei der SoftVGA zu 1500ohm bzw 680ohm wurde). Diese werden um die Spannungen zu kombinieren derart an Pins gehängt, dass der 2/3 Fall mit dem 680ohm an ein höherwertiges Bit und der 1/3 Fall mit dem 1500ohm an ein niederwertiges kommen. Diese Paare kommen dann gleich nebeneinander für je einen der 3 RGB Pins vom VGA Stecker.

Am Schluss wurde daraus Bit5+4 R und Bit3+2 G und Bit1+0 B, mit Bit5+3+1 680ohm und Bit4+2+0 1500ohm, zusammen als RRGGBB (auch 2R2G2B geschrieben) nutzbar, mit 000000=Schwarz und 111111=Weiss, sowie 110000=Rot 001100=Grün und 000011=Blau, jedes der drei beliebig mit 00=0/3 01=1/3 10=2/3 und 11=3/3 an Intensität, und diese wiederum beliebig mischbar für 64 Farben (die selbigen 64 wie sie auch beim Sega Master System und der IBM EGA verwendet werden).

Strikte kann man diesen Trick mit 3*3=9 Pins und davon 8*8*8=512 Farben RRRGGGBBB (bzw 3R3G3B) (wie bei Sega Mega Drive / Genesis und Atari ST) erweitern, oder gar mit 3*4=12Pins und davon 16*16*14=4069 Farben RRRRGGGGBBBB (bzw 4R4G4B) (wie bei Commodore Amiga). Aber der AVR kann wegen 8bit Prozessor und Ports keine 9 oder gar 12 Pins gleichzeitig schalten. Man kann aber trotzdem die volle 8 ausnutzen, aber nur mit "ungeraden" Farben erzeugen, welche Farbfehler beinhalten, welche ich mit einer Analyse der Farben angeschaut habe.

Ein Ansatz dazu ist 3+3+2 Pins zu 8*8*4=256 Farben RRRGGGBB (bzw 3R3G2B) (wie bei Yamaha V9958 bzw MSX 2+ im 8bit Modus). Problem dabei ist, dass man bei Rot und Grün dann 8 Stufen 0/7..7/7 hat, aber bei Blau nur 4 Stufen 0/3..3/3, was bei Graustufen zuviel Blau und davon Blaustich oder zuwenig Blau und davon Gelbstich erzeugt. Siehe obige Analyse ab Titel "3|3|2Bits/Channel, reduced blue, 8bit RRRGGGBB, 256 Colours", wie nach der Zeile "The resulting 8 R+G driven "gray" levels:" diese verschmutzt wirken. Weshalb ich diesen Ansatz sofort verworfen habe. (Die Uzebox verwendet diesen Ansatz.)

Ein anderer Ansatz benutzt alle 3 mit 8 Stufen 0/7..7/7, teilt aber das niedrigste Bit von Grün und Blau, für 256 von 512 Farben. Damit sind Graustufen ohne Farbstich, aber bei manchen Helligkeiten werden Grün und Blau unregelmässig. Siehe obige Analyse ab Titel "3Bits/Channel, merged G+B LSBs, 8bit RRRGGBBL, 256of512 Colours". Wobei Diagonalen korrekt sind, aber wenn man 2x2 Nachbaren anschaut deren Helligkeit streuen. Weshalb ich diesen Ansatz auch bald verworfen habe.

Die 64 RRGGBB Farben reichen ohnehin, hatten historische Homecomputer und Spielkonsolen auch nur irgendwo im Bereich 8..256 an Farben. Und ein guter Nebeneffekt von 3*2=6bit benutzen waren genau 8-6=2 vorige Pins am Port, welche ich gleich für die beiden digitalen Leitungen für die Synchronpulse nutzen konnte, Bit7 Vertikal und Bit6 Horizontal. Was dann mit obigen Bit5..0 zusammen zu Bits7..0 in Anordnung VHRRGGBB führte.

Das erlaubte auch einen flexiblen Hardwareaufbau, bei dem alles analoge (die 6 Widerstände und der VGA Stecker und deren Verdrahtung) auf eine kleine Hilfsplatine kommt, welche an einem beliebigen einzelnen AVR Port passt. (Das solange dieser zu einem STK200 bzw STK500 kompatiblen 5x2=10 Pin Pfostenstecker verdrahtet worden ist. Dieser hat auf Pins1..8 = BIT0..7 = P[A..D]0..7 sowie Pin9 = 0V und Pin10 = 5V.) Der VGA Stecker bekommt an diesem dann VGA Pin1 = Rot zu BIT5+4 = STK200 Pins6+5 = Port5+4, Pin2 = Grün zu BIT3+2 = Pins4+5 = Port3+2, Pin3 = Blau zu BIT1+0 = Pins2+1 = Port 1+0, Pin4+5 leer, Pin6+7+8+9+10 zu 0V = Pin9 ohne Port, Pin11+12 leer, Pin13 = H-Sync zu BIT6 = Pin7 = Port6, Pin14 = V-Sync zu BIT7 = Pin8 = Port7, Pin15 leer.

Danach brauchte ich nur noch eine generische STK200 bzw STK500 kompatible AVR Experimentierplatine mit 4 solchen Pfostensteckern, plus einem ebenfalls 5x2=10 Pin STK200 kompatiblen Programmierstecker (aber nicht STK500 kompatibel, dieser brachte einen anderen 3x2=6 Stecker). Deren Schaltung ist inzwischen als Verdrahtung textuell beschrieben, und wurde vor langem auch als Handskitze graphisch abgebildet. Letzteres mit linkes Blatt AVR Beschaltung von oben angeschaut, mittleres Bild Platinenlayout und teilweise AVR Verdrahtung von unten angeschaut, rechtes Bild die Hilfsplatine VGA Ausgang Verdrahtung von unten angeschaut. Danach wurde die Platine hergestellt, bestückt, und getestet. Addiert man noch ein 5V Steckernetzteil und fertig ist die sehr einfache Hardware.

Logisch braucht es dann noch einen STK200 kompatiblen PC LPT Parallelport AVR Programmieradapter (ist ebenfalls in obiger Verdrahtung mit beschrieben), plus 10pin Bandkabel an die Experimentierplatine, und ein DB25 Kabel für an den LPT Port. Dabei entstand auch gleich ein UART zu RS232 Adapter mit einem MAX232, welches zwischen Programmieradapter und Experimentierplatine eingeschleift wird, plus 10pin Bandkabel an Experimentierplatine, und ein DB25 Kabel für an den COM Port. Ein RS232 LED Tester und sein LPT Parallelport Gegenstück sind dabei auch nützlich. Ein RS232 Nullmodem sowie Adapter von COM Port 9pin an RS232 25pin waren bereits vorhanden.

All diese Hardware wurde dann hergestellt. Oben links auf dem Laptop VGA Ausgang stehend der LPT Tester laufend (mit 13+4 2.5mm LEDs). Darunter mitte AVR Programmieradapter und dessen Bandkabel. Rechts etwas weiter oben der UART zu RS232 Adapter und dessen Bandkabel. Darunter rechts ist die Hilfsplatine VGA Ausgang. Unten mitte ist die AVR Experimentierplatine. Weiter kann man die Hardware im laufenden Zustand sehen. Unten links AVR mit Hilfsplatine zu VGA Stecker. Unten mitte der UART zu RS232 Adapter und darüber RS232 Tester (mit 3+4 5mm LEDs) und Nullmodem und Kabel. Rechts davon AVR Programmieradapter an LPT Tester und Kabel. Weiter sieht man links ganz oben, dass der Laptop auf einer Dockingstation steht, welcher die LPT und COM Ports stellt.

Wegen der Zweitbelegung aller 4*8=32 PIO Pins der ATmega32 mit diversen Zusatzfunktionen, habe ich den Port PB gemieden weil dort der Programmiervorgang (und SPI) die Pins PB4..7 benutzt und Interrupt2 Pin PB2 liegt, sowie PD weil dort die UART Pins PD0..1 und Interrupts0+1 Pins PD2..3 liegen, und ebenso PA weil dort die Analogeingänge für allfällige Paddles sind. Damit verblieb als bester noch Port PC der nur Echtzeituhr Quarz und JTAG und I2C/TWI hat. Dazu kam später noch an Port PD eine Hilfsplatine mit 8 DIP Schalter um Optionen einzustellen.

Generell kann man den ganzen Ablauf vom Projekt sehr detailliert nachlesen, von Idee über Design und Planung bis Hardware und Software erstellen, im Logfile des Projektes. Viele der Details in diesem Artikel sind aus dem Logfile extrahiert, wären ohne längst vergessen worden!

Im Nachhinein sind in 10 Jahren ein paar Designfehler aufgefallen bei der bestehenden Platine in ihrem Gebrauch:

Die Wahl von Port PD für die Optionen Schalter war nicht optimal, weil dies mit benutzen der UART kollidiert. Dies wurde bereis März 2009 an einer Demo festgestellt. Schon bei wiederaufnahme der ATmega32 Entwicklung wird die neue Software Version dafür PA nehmen, da Paddles inzwischen keine Relevanz mehr haben.

Den Programmierstecker erweiterten mit zudem die UART daran verbinden hat sich auch nicht bewährt, weil diese den Port PD stört (und damit auch die Optionen Schalter dort) wenn der UART zu RS232 Adapter nicht zwischen Programmieradapter und Experimentierplatine drin ist. Diese Verbindung habe ich inzwischen auf der alten Platine durchgetrennt. Die neue ATmega644 Platine wird die UART auf einen separaten 5x2=10 Stecker legen. Dieser mit Pin1=GND Pin3=leer Pin5=VCC, Pin7=TxD, Pin9=RxD, und Pins2..10 die Hilfsssignale DCD DST DTR CTS RTS. Weshalb auch der UART zu RS232 Adapter leicht modifiziert werden muss, ohne durchschleifen, nur TxT RxD sowie CTS RTS benutzend. Allenfalls wird dann auch die ATmega32 Platine passend modifiziert, falls ein Stecker Platz findet.

Zudem ist es klar geworden, dass ich neben ansteuern mit UART (als Terminal) und SPI (eher für debuggen) auch mit PIO (für Apple 1 artige Recher) unterstützen will. Also wird die neue ATmega644 Platine bei PA und PC mit Handshake Leitungen erweiterte STK200 bzw STK500 Pfostenstecker auf 8x2=16 Pin bekommen. Dies mit Timer Pins benutzen, damit direkte Hardware Reaktion von Ein (Timer zählen) zu Aus (Timer Überlauf) möglich ist. PA bekommt für Pin15=Aus PB3(OC0A+AIN1) und Pin16=Ein PB0(T0+XCK0), PC bekommt für Pin15=Aus PD5(OC1A) und Pin16=Ein PB1PB1(T1+CLKO). Dazu könnte noch PB2(INT2) an beide ihre 13=InterruptEin. Allenfalls wird dann auch die ATmega32 Platine passend modifiziert, wofür Platz erwiesen vorhanden ist.

Software Video Grundlagen

Kernpunkt von allem mit einem Mikrocontroller generierten Video ist, die gewollten Pixelfolgen ihre Farben mit einer Serie von Ausgabebefehlen an den entsprechenden PIO Port zu schreiben. Das genug schnell und zum genau richtigen Zeitpunkt und mit den passenden Farbwerten machen ist die ganze Kunst!

Dieser Ansatz ist nicht neu, wurde er doch schon bereits in den Sinclair ZX80 und ZX81 Rechnern teilweise benutzt. Dort musste allerdings noch dem weit langsameren Z80 Prozessor mit externer Hardware nachgeholfen werden, und es kam dabei trotzdem nur Schwarzweiss ohne Graustufen heraus. Dies weil die Z80 nur 4MHz konnte (und real mit 6.5/2=3.25MHz lief), was sich wegen 4 Takte/Opcodezugriff zudem in nur 1MHz (bzw real 3.25/4=0.8125MHz) Speicherzugriffe umsetzte, also unter 1/16 der Geschwindigkeit des 16MHz AVR lag. Das erlaubte nur 1 Speicherzugriff pro Zeichen von 8 Pixel ausgeben. Also wurden dort mit dem Prozessor nur die Zeichencodes aus dem SRAM geholt, während per externer Logik diesem ein NOP vorgegaukelt wurde. Gefolgt von dann dessen Refreshlogik und die Interruptvektortabelle Basiadresse missbrauchen um die Pixeldaten des Zeichens aus dem ROM zu holen, mit dem Zeichencode statt der ohne DRAM unbenutzen Refreshadresse und der ohne IM2 Modus unbenutzten Vektortabelle auf den Font zeigend. Gefolgt von danach die Pixeldaten in ein 8bit Schieberegister laden welches mit 8*0.8125=6.5MHz die Pixel ausgab. Womit dies halb-Software Video ist.

Hier in der SoftVGA, mit ihrer weit schnelleren AVR, geschieht alles in reiner Software, mit deren out Befehl um Pixel auszugeben, mit dem Port PC als Ziel und einem der 32 AVR Register als Datenquelle, in dem die Pixelfarbe in Format von xxRRGGBB darin steht. Um mehrere Pixelfarben zu haben für Vordergrund und Hintergrund, werden mehrere Register mit verschiedenen xxRRGGBB vorbesetzt, welche zusammen die Colour Look Up Table (CLUT) der Anzeige bilden, vergleichbar zu denen in Atari 800 und CPC und EGA/VGA. Um ein Pixelmuster zu erzeugen wird eine Serie von out Befehlen gebraucht, welche diese Farbregister abwechselnd ausgeben. Am Zeilenende muss die Farbe auf Schwarz gehen, was mit out eines weiteren Blanking Registers mit dem Wert xx000000 geschieht.

Die xx erzeugen die V und H Syncsignale, und sind bei einem Multisync Monitor egal welcher Wert, solange sie in der Zeile drin konstant bleiben, und beim Sync auf das Gegenteil davon gehen. Was hier anfangs mit weiteren out mit passenden xx000000 (mit allen anderen 3 xx Werten) gemacht wurde. Aber später um Register zu sparen mit cbi und sbi Befehlen welche einzelne Portbits und Pins auf 0 bzw 1 setzen ohne die anderen vom gleichen Port zu verändern. (Mit dem originalen Festfrequenz VGA Monitor war Normalzustand mit VH=11 in der Zeile notwendig um Graphik 60 Bilder/Sekunde mit 480 von 525 Zeilen Modus zu verlangen, sowie VH=01 um Text 70 Bilder/Sekunde mit 400 von 450 Zeilen Modus zu verlangen. Davor waren VH=00 gewesen bei CGA mit 200 von 262 Zeilen, sowie VH=10 bei MDA und EGA mit 350 von etwa 364 Zeilen.)

Ein out Befehl braucht genau einen Taktzyklus. Damit kann man strikte mit einer direkten Serie von out die Pixel mit der vollen AVR Taktfrequenz erzeugen, und damit 480 Pixel/Zeile. Aber dies würde danach verlangen, jede mögliche Videozeile als eine lange out Befehl Serie im Flash des AVR zu haben, und nur erlauben diesen spezifischen Zeilensatz in beliebiger Zeilenfolge abzuspielen. Was aber graphisch komplett unbrauchbar ist. Also müssen Zeilen zur Laufzeit/Zeichenzeit aus Pixelsegmenten zusammengesetzt werden. Dies verlangt zwar nur nach einem kleinen Satz von kurzen Segmenten, aber auch nach zur Laufzeit/Zeichenzeit von Segment zu Segment zu springen, wozu die Logik alles zwischen den out Befehlen geschehen muss, was deren theoretische Pixelfolge reduziert.

Da als Ziel 40 Zeichen pro Zeile angestrebt werden, mit je 4 Pixel Breite, liegt es nahe, genau 2^4=16 Pixelsegmente zu definieren, welche alle 16 möglichen Pixelkombinationen von Vordergrund und Hintergrund Farbe zeichnen. Das ergibt in diesen mindestens 4 out Befehle, mit den jeweilig passenden beiden CLUT AVR Register, und somit 4 Taktzyklen beim zeichnen, plus was auch immer nötig ist um das nächste Pixelsegment zu identifizieren und anschliessend ausführen.

Da der variable Sprung von einem Pixelsegment zum nächsten einen ijmp Befehl braucht, der 2 Taktzyklen lang ist, ist zumindest das letzte Pixel auf mindestens 1+2=3 Taktzyklen Breite festgelegt. Für konsistent breite Pixel ist daher mit mindestens 4*(1+2)=12 Taktzyklen pro Segment zu rechnen, was neben den 4 für out und 2 für ijmp noch ganze 3*2 für das nächste identifizieren lässt! Sollten diese 12 nicht ausreichen müssten 4*(1+3)=16 oder gar 4*(1+4)=20 Taktzyklen benutzt werden, was aber entweder 4/3 oder gar 5/3 mal soviel Taktfrequenz braucht, oder die Zeichen pro Zeile reduziert auf 3/4 oder gar 3/5 von 40 zu 30 oder gar 24.

Für die angestrebten 25.175/2/2=6.294MHz für 40*4=160 Pixel brauchen 3 Taktzyklen pro Pixel 3*6.294=18.881MHz, also für die ATmega32 ihre 16MHz um +18% übertaktet. Was sie aber locker aushält. Schliesslich sind die 16MHz für die "Industrial" Spezifikation von 4.5..5.5V und -55..+125°C garantiert. Wobei mehr Spannung und weniger Temperatur jegliche Chips schneller macht, also obiges selbst bei den schlechtesten 4.5V und +125°C gelten muss. Ich habe nie über +40°C, kann mehr Takt benutzen, (125+273)/(40+273)=1.27=+27%, was von 16MHz auf 20.3MHz erlaubt. Zudem liefert mein verwendetes Steckernetzteil 5.2V, und damit auf min 5.0V erhöhen erweitert Takt um 5.0/4.5=1.11=+11%. Was zusammen dann +41% gibt, und somit von 16MHz auf 22.6MHz erlaubt. Und das nur mit optimaleren Umgebungsbedingungen geben!

Dazu kann man noch die Herstellung ihre Exemplarstreuung als Reserven ausnutzen, wie es PC Overclocker machen. Das erlaubt der Uzebox ihre ATmega644 von 20MHz auf 28.636MHz um +43%(!) zu übertakten. Wobei sie festgestellt haben, dass nur mit Reserven und Temperatur ausnutzen bereits 90% der AVR Chips das aushalten, und die restlichen 10% kommen mit Overvolting auch noch zum laufen.

Real betreibe ich die SoftVGA ATmega32 wegen vorhandenen Quarzen mit 18.432MHz (+15%), was mit auf 18.881MHz berechnetem Timing den Monitor etwas untertaktet (-2.5%), was dessen Analogelektronik aber auch locker wegsteckt. Die SoftVGA ATmega644 wird wegen vorhandenen Quarzen mit 24MHz (+20%) betrieben werden, was mit auf 25.175MHz Timing den den Monitor etwas mehr untertaktet (-4.6%).

Testbild Generator

Jeder der schon mal ein Videogame programmiert hat weiss, dass man zuerst die Ausgabe schreibt, weil Fehler in dieser direkt sichtbar sind, und erst danach die Gameengine, weil Fehler in dieser nur via falsche Ausgabe verlangen sichtbar werden. Selbiges gilt bereits für beliebige technische Visualisierungen, oder gar für Büroanwendungen. Logisch gilt dies genauso hier, gerade wo die Ausgabe generieren schon Software ist. Also wurde zuerst ein Testbild Generator geschrieben um die Hardware und Programmiertechniken auszutesten.

Da eine VGA Zeile bei 25.175MHz 640 Pixel und Takte gezeichnet hat, plus 1/4 davon rücklaufend 160, für insgesammt 800, haben wir hier bei 6.294MHz Pixel bzw 18.881MHz Takt noch 1/4 bzw 3/4 davon, also 200 Pixel (mit 160 gezeichnet und 40 rücklaufend) bzw 600 Takte (480 zeichnend und 120 rücklaufend) pro Gesammtzeile. Der Rücklauf wird zudem noch in 1/4+1/4+2/4 Normal+Sync+Normal aufgeteilt, also zu 30+30+60 Takte. Selbiges geschieht beim Bild, aus 400 (oder 480) Zeilen gezeichnet, plus noch 50 (bzw 45) rücklaufend, für insgesammt 450 (bzw 525). Der Rücklauf zudem auch in 1/4+1/4+2/4 aufgeteilt zu 13+12+25 (bzw 11+11+23).

Programm "VGA Static"

Daraus ergibt sich die erste Softwarestruktur, verwendet in diesem Testbild Generator "VGA Static". Programm ist in der Datei vga_static.asm. Es ist in Assembler weil irgendetwas anderes für solche Zeitanforderungen schlicht nicht geht. Anfangs kommt die AVR vorbereiten. Das ist alles mit "need fuses", die .include bis .org, die jmp Befehle für Reset und Interrupts, die leere Interrupt Serviceroutine RETI:, sowie den Reset behandeln ab RESET:.

Eigentliches Video fängt an bei COLOURS:, wo die AVR Register für die Colour Look Up Table (CLUT) mit Farbwerten geladen werden, ebenso die Werte für Syncpulse. Ab DDR: wird der Port PC für die Ausgabe vorbereitet. Ab LOOP: läft das eigentliche Programm, welches pro Schleifendurchlauf einmal das Bild ausgibt, in 1/70s, was 70Hz Bild ergibt. Das ist eine Endlosschleife, bis Strom weg oder Reset, weil alle Videogeneratoren in Hardware genau so laufen.

Dieses besteht aus 4 Takte für den call FRAME Befehl, dann 449 Zeilen zu 600 Takte sowie eine letzte Teilzeile zu 583 Takte in FRAME drinnen, und dann noch die 13 verbleibenden Takte teilweise mit nop aufbrauchen bevor mit jmp LOOP alles wiederholen. Die letzte 450ste Zeile in FRAME (oder eines beliebigen Zeilenblockes) wird dazu stets bewusst gekürzt, damit für nop + jmp + erneut call Zeit verbleibt. Damit entsteht ein VGA Textmodus Signal mit 400 dargestellten von 450 ausgegebenen Zeilen und 70 Bilder/Sekunde Timing, mit folglich diese Schleife 70 mal in der Sekunde durchlaufen. Man beachte, dass jeder Befehl seine Taktzahl in Kommentaren aufgelistet hat, um diese aufzusummieren, damit die richtige Anzahl herauskommt. Ohne diese Konvention kann man so etwas wie Software Video Timing überhaupt nicht in den Griff bekommen.

Ab FRAME: wird das Bild ausgegeben. Das in der Reihenfolge, ab FRPULSE: vertikaler Syncpuls 12 Zeilen mit call VRLINE und call WAIT. Dann ab FRBLANK: vertikales Normal 25 Zeilen mit call BLLINE und call WAIT. Dann ab FRDRAW: Bild zeichnen 400 Zeilen (80 mit call PXLINE, 40*2 abwechselnd call PXLINE und call BLLINE, 80 call BLLINE, 40*2 abwechselnd call SHLINE und call BLLINE, 80 call SHLINE) alle mit call WAIT. Dann ab FREND: vertikales Normal 12+1=13 Zeilen (12 call BLLINE mit call WAIT, sowie 1 letztes ohne diesem), sowie zum Abschluss ret. Da das letzte call BLLINE kein call WAIT hat fehlen Taktzyklen, genau die welche dem ret und in LOOP den nop + jmp + wieder call FRAME ihre Zeit geben.

Ab VRLINE: wird eine Leerzeile mit V-Sync Bit7 = 1 ausgegeben. Wieder in Reihenfolge ab VRPULSE: horizontaler Syncpuls 30 Takte mit dem out PORTC,VHR (VHR ist Register R7 mit 0xC0 drin, V-Sync und H-Sync), dann ab VRBLANK: horizontales Normal plus Bild zeichnen 60+480 Takte mit dem out PORTC,VER (VER ist Register R6 mit 0x80 drin, nur V-Sync). Ab VREND: wären eigentlich noch 30 Takte horizontales Normal angebracht, aber diese werden stets nach dem out PORTC,VER weggelassen, wie oben schon nach dem letzten call WAIT, wieder um dem ret und in FRPLOOP den call WAIT + dec TH + brne FRPLOOP gefolgt von wieder call VRLINE Zeit zu geben.

Ab BLLINE: wird fast genau selbiges wiederholt, nur mit out PORTC,HOR (Register R5 mit 0x40 drin, nur H-Sync) und out PORTC,BLK (Register R8 mit 0x00 drin, kein Sync). Was alles ebenfalls eine Leerzeile erzeugt, aber ohne den vertikalen Syncpuls drin.

Ab PXLINE: wird endlich sichtbares Bild erzeugt. Anfangs ab PXPULSE: selbigen horizontalen Syncpuls ohne vertikalen wie in BLLINE, aber dann statt 60+480 horizontales Normal plus Bild schwarz, ab PXBLANK: nur 60 von ersterem und dann ab PXDRAW: eine Bildzeile ausgeben. Diese besteht anfangs ab PXWB: aus 2*1 + 2*2 + 2*4 + 2*8 + 2*16 + 2*32 = 126 Takte abwechselnd weiss mit out PORTC,WHT (Register R15 mit 0x3F) und schwarz mit out PORTC,BLK. Dann ab PWRAINBOW: aus 7*1 + 7*2 + 7*4 + 7*8 = 105 mit Farben rot gelb grün cyan blau magenta und schwarz. Dann ab PXSEPAR: 9 Takte weiss/schwarz. Dann ab PXRGB: aus 8*30 = 240 Takte alles 8 Grundfarben. Wie gehabt dann noch ab PXEND: out PORTC,BLK für horizontales Normal anfangen, und dann ret damit dieser und FRPLOOP Zeit haben.

Ab SHLINE: wird fast genau selbiges wiederholt, nur mit ab SHDRAW: eine andere Bildzeile ausgeben. Die fängt an mit 16*(2*3) Takte weiss/schwarz, wobei die ersten 3 Takte weiss "nebenbei" TL=15 setzen mit ldi TL,15, dann 15 schwarz/weiss TL=TL-1 machen mit dec TL in schwarz bzw mit brne SHWLOOP in weiss, und danach das letzte schwarz die ldi AL,0x00 und ldi TL,0x3F vorbereiten. Dieses "parallel" während einer Taktserie vorbereiten ist die Kerntechnik um später einen Font Zeichen für Zeichen in einer Zeile auszuwerten. Entscheidend ist, dass der Port ja die VHRRGGBB Daten festhält, der Prozessor daher beliebiges machen kann, solange er zum richtigen Zeitpunkt das passende nächste out PORTC,???? macht. Dann ab SHRRGGBB: kommen 64*6 Takte mit alle AL Werte von 0x00..0x3F ausgeben, also alle 64 möglichen Farben. Wieder kommt die letzte Farbe erst nach der Schleife dran, mit daher noch 5 nop "aufgerundet". Dann ist ab SHEND: wie gehabt.

Ab WAIT: ist schliesslich die wiederholt benutzte Routine, welche für alle Wartereien von mindestens 11 Takte benutzt wird. Das wegen 1 Takt ldi TL,??? plus 4 Takte call WAIT, sowie TL*3-1+4 Takte darin ablaufend. Für TL = 1..256(=0) ergibt es 1+4+(1*3-1+4)=11 .. 1+4+(256*3-1+4)=776, in 3er Stufen ansteigend. Alle kleineren Wartereien müssen mit nop Serien gemacht werden. Ebenfalls müssen wegen den 3er Stufen alle grösseren für WAIT "abgerundet" werden, und danach noch mit 1 oder 2 nop danach "aufgefüllt" werden.

Der ganze Code besteht somit nur aus stupidem herausbrettern von vielen out PORTC,????, nur eben genau die richtigen Folgen davon, und mit exaktem Timing durch Wartezeiten dazwischen, damit der Monitor ein geometrisch stabiles Bild anzeigt. Damit konnte ich ein Testbild erzeugen, ohne Text, nur Farbstreifen. Dieses lief im ersten Anlauf bereits reibungslos, wenn auch damals noch mit PXLINE wiederholt statt SHLINE. Es zeigte aber mit den falschen Farben einen Lötfehler auf der VGA Stecker Hilfsplatine auf, weil die Rot und Grün Signale ihre Bit0 mit einer Lötbrücke kurzgeschlossen waren. Korrigiert ergab es das angezielte Bild. Dann habe ich noch um alle Farben zu sehen SHLINE addiert, welches ein besseres Bild ergab. Das ist dann die archivierte Version von diesem ersten Programm, das ich hier gerade durchgegangen bin.

Textausgabe statt Bitmap

Für Text ausgeben erschwerend kommt jetzt noch dazu, dass das SRAM der AVR niemals ausreichen kann, um für alle Pixelsegmente aller Zeilen einen Musteridentifikator zu speichern. Dies würde bei 40 Segmente pro Zeile mal 200 Zeilen nach 8000 Identifikatoren verlangen, was selbst bei nur 1Byte pro Identifikator 8000Bytes braucht, welche nicht in die 2k=2048 Bytes an SRAM der AVR passen kann.

Mit nur 20 Segmente pro Zeile, aber mit je 8 Pixel Breite, 2^8=256 davon, und 2 Zeichen in jedes hineinrendern, mal nur 100 Zeilen davon, würden 2000 Identifikatoren gerade noch ins SRAM passen. Aber die 100 statt 200 Videozeilen sind für Videogames schon unterste Grenze (was die 160 Pixel ebenfalls schon sind), aber erlauben vor allem nur noch 100/8=12 Textzeilen, was weit zuwenig ist, oder allenfalls noch 100/7=14 oder 100/6=16 was aber bald zu unleserlich wird. Daher ist eine universelle Bitmapgraphik mit dem vorhandenen ATmega32 wegen zuwenig SRAM nicht wirklich machbar. (Mit den 4k einer ATmega644 geht dies aber gerade noch.)

Es muss statt dessen auf eine Textgraphik gezielt werden (wenn möglich mit Blockgraphik addiert), welche nur noch pro Textzeichen Zelle einen Zeichenidentifikator braucht, und damit nur pro Zeichenzeile 40. Was bei den angezielten 40x25 (oder 40x30) Zeichen nur nach 1000 (bzw 1200) Bytes verlangt, und so locker in die 2k SRAM passt. Problem dabei ist aber, dass man jetzt vom gleichen Zeichenidentifikator je nach der Videozeile innerhalb der Zeichenzeile zu verschiedenen Pixelsegmenten und somit deren Musteridentifikatoren kommen muss, was die Software leicht aufwendiger macht. Und Zeit dazu hat es nur wenig.

Algorithmus "Direct"

Das Projekt fing realistisch an, als es mir gelang nach am VCFe 2008 das AVR-ChipBasic sehen, einen Zeichenalgorithmus zu finden, der dies alles in obigen 12 Taktzyklen schafft! Der Ansatz besteht darin, dass die 4 out Befehl plus maximal 4*2 weitere Taktzyklen an Befehlen dazwischen auf maximal 4*(1+2)=12 Takte kommen, und somit auch maximal 12 Befehlsworte hinauslaufen können (real wurden es 10 Befehlsworte wegen 2 Stück 2-Takt Befehlen drin). Diese kann man zwecks schneller Rechnung der Sprungadressen auf Blöcke von 16 Befehle ausdehnen. Danach wird für jede der 8 Zeilen der maximal 256 Zeichen diese 16 Befehl Routine wiederholt. Wegen 2 Bytes pro Befehl sind das 8Zeilen*16Befehle*2Bytes = 256 Bytes/Zeichen. Für max 256 Zeichen dann max 256*256=64k Flash.

Dabei müsste der Zeichencode genau mal 256 gerechnet werden, was schlicht mit ins High Byte kopieren und Low Byte auf 0 setzen gehen sollte. Bis man sich aber errinnert, dass der AVR seinen 16bit Programmspeicher mit Wortadressen anspricht, und nicht mit Byteadressen! Womit der ijmp folglich in Registerpaar ZH:ZL (Register 31 und 30) solche sehen will, was dann 8*16*1 = 128 Worte/Zeichen ausmacht, bzw 2 Zeichen in 256 Worte passen. Eine 16bit Multiplikation mit 128 kann man wegen weit zu langsam vergessen. Dies kann man aber wegen genau 16 einfach ersetzen, mit Bitmanipulation, indem man die Zeichencodes in die Low und High Bytes kopiert, und passende Bits mit AND ausfiltert. Dabei werden aus einem 8bit Zeichencode die Bits6..0 extrahiert, welche für den ijmp zu Programmadresse A14..8 werden, und separat das Bit7, welches zu Adresse A7 wird, mit dann A6..0 stets 0 egal ob 0 oder 128 als Anfang vom Zeichen ist, und A15 als allfälligen Fontauswähler. Womit die Zeichen 0x00..0x7F dann in den ersten 128 Worten von256er Pages liegen müssen, und 0x80..0xFF dazwischen in den verbleibenden zweiten 128 Worte einfügt werden, also die Zeichencodes nicht mehr linear im Flash sind.

Korrigieren muss man dann noch, das selbst nur ein Font von (128*2=256)*128 = 32kWorte = 64kBytes nicht in die 32kBytes Flash der ATmega32 passt. Also nur (64*2=128)*128 = 16kWorte dort hineinpassen, für 128 Zeichen mit Zeichencodes 0x00..0x3F und 0x80..0xBF. Davon muss man noch den Platz für sonstigen Programmcode abziehen, was realistisch in maximal etwa 56*2=112 Zeichencodes von 0x08..0x3F und 0x88..0xBF resultiert.

Was immer noch ausreicht für volles 95 Zeichen ASCII 32..126, plus 16 Zeichen Blockgraphik in pseudo-ASCII 16..31. Diese Codes ASCII 16..126 müssen aber beim Speicher beschreiben auf Zeichencodes 8..63 und 136..191 umgerechnet werden, damit diese auswerten/ausgeben schnell genug ist. Was aber einfach geht, mit schlicht mit Shift Rechts durch 2 teilen, womit ASCII Bits7..1 zu Zeichencode Bits6..0 werden und Bit0 via Carry zu Bit7 wandert. Danach tut obige Bitmanipulation Bit7 belassen, und Bit6..0 nach A14..8 schieben. Der ATmega32 kann dann A14 nicht verwerten, weshalb dieses 0 sein sollte, also auch Bit6 im Zeichencode, und somit Bit7 in ASCII Code, was ohnehin der Fall ist.

Der Programmcode für ein Segment besteht dann aus 4 out PORTC,????, wobei ???? entweder FORE oder BACK ist, mit diese wiederum 2 Register R10 und R11, welche anfangs 0'0'11'11'00 = 0x3C = gelb und 0'0'00'00'11 = 0x03 = blau beinhalteten. Damit werden die 16 Segmentsorten definiert und gezeichnet. Dazwischen kommt aber "parallel" das nächste Segment bestimmen. Mit nach erstem out ein ld ZL,X+ (2 Takte) um dessen Zeichencode vom SRAM zu holen und zugleich Registerpaar XH;XL (Register 27 und 26) +1 zu rechnen. Dann kommen nach zweitem out ein mov ZH,ZL (1 Takt) um eine Kopie zu erzeugen, sowie ein erstes andi ZH,0x3F (1 Takt) um die Bits5..0 auszufiltern. Dann kommen nach drittem out ein andi ZL,0x80 (1 Takt) um das Bit7 auszufiltern, sowie ori ZL,0x?0 (1 Takt) mit ? = 0..7 um das Segment innerhalb des Zeichens anzuwählen. Zuletzt kommt nach viertem out das ijmp (2 Takte) um das Segment vom nächsten Zeichen auszuspringen. Technisch entspricht dies einem Tokeninterpreter. Nach nun 4 out + 1+2+2+1 anderen Befehle und Befehlsworten, müssen auf 16 hinauf noch 16-10=6 Worte mit nop gefüllt werden. Das Resultat sieht so aus:

out PORTC,????  ; 1 Takt,  1 Wort,  Pixel 1 ausgeben, ???? = FORE oder BACK
                ;                   aus 4* FORE oder BACK entstehen Pixelmuster
ld ZL,X+        ; 2 Takte, 1 Wort,  Zeichennummer von SRAM holen und +1

out PORTC,????  ; 1 Takt,  1 Wort,  Pixel 2 ausgeben
mov ZH,ZL       ; 1 Takt,  1 Wort,  kopieren Zeichennummer
andi ZH,0x3F    ; 1 Takt,  1 Wort,  Limite 64 Pages a 2 Zeichen je 128 Befehle
                ;                   volle 128 Pages a 2 Zeichen waere mit 0x7F

out PORTC,????  ; 1 Takt,  1 Wort,  Pixel 3 ausgeben
andi ZL,0x80    ; 1 Takt,  1 Wort,  welches der beiden 128er in Page
ori ZL,0x?0     ; 1 Takt,  1 Wort,  welches der 8 Videozeilen, ? = 0..7
                ;                   Mehraufwand Textgraphik statt Bitmapgraphik

out PORTC,????  ; 1 Takt,  1 Wort,  Pixel 4 ausgeben, das zwischen den Zeichen
ijmp            ; 2 Takte, 1 Wort,  naechstes Zeichen sein Segment ausgeben

nop             ; 0 Takte, 6 Worte, Rest vom 16er Block pro Segment füllen
nop
nop
nop
nop
nop

Um diese Kernroutine herum, 40 mal in einer Zeile, für 40*12=480 Takte, wird der Rest aufgebaut. Grösstes Problem dabei ist offensichtlich, dass es mangels Zeit keinen Platz hat für Befehle um die 40 Zeichen der Zeile abzuzählen, jede Zeile also unendlich lange so weiter gehen würde! Aber wir haben Zeit, und zwar nach dem letzten Zeichen, bis das erste der nächsten Zeile dran kommt, also während dem Rücklauf. Genauer sogar 600-480=120 Takte davon, in denen wir nur nach 30 und 60 zwei weitere out PORTC,???? für den horizontalen Syncimpuls Anfang und Ende erzeugen müssen.

Dazu wird im SRAM nach den 40 Bytes der Zeichen ein 41stes Zeilenabbruch Pseudozeichen hingestellt, welches den Tokeninterpreter von zeichnen zu rücklaufen umschaltet, nach bereits "parallel" zum 40sten Zeichen wie gehabt bestimmt worden sein. Dieses wurde anfangs mit Zeichencode 0x07 gemacht, in Page 7, um davor Platz für 0..6=7 Pages an Programm zu lassen, mit ab 0x08 dann 64-8=56 Pages zu je 2 Zeichen. Dieses macht pro Segment genau nur 2 Sachen: Als erstes ein out PORTC,NONE (1 Takt), wobei NONE Register R15 ist, welches stets 0'0'00'00'00 = 0x00 = schwarz ohne Sync beinhaltet als Blanking Register. Damit wird egal welche Farbe das letzte Zeichen beendete auf schwarz gestellt. Dann kommt jmp DREND? (3 Takte), mit wieder ? = 0..7. Was dort geschieht sieht man an besten wenn man den Resten vom Programm mit dem Testbild Generator vergleicht. Daraus entstand der erste Ansatz "VGA Text Static" des Algorithmuses "Direct".

Programm "VGA Text Static"

Aufbauend auf diesen Algorithmus gab es 6 Programmversionen:

Programm "VGA Text Static" Version 1

Gleich geblieben ist vieles von der Softwarestruktur. Siehe hierzu die erste Version. Das Programm ist in vga_text_static.asm, der Font dazu in vga_text_font.inc.

Der obige Algorithmus steckt eigentlich wiederholt verwendet im Font, wird aber ganz am Ende vom eigentlichen Programm einmalig als Kommentare dokumentiert, bevor ein .include "vga_text_font.inc" als letzte Zeile darin den Font mit all seinen passend besetzten Kopien der Segmentroutine dazulädt, nach gerade davor das Zeilenabbruch Pseudozeichen 0x07 auscodiert haben. Hier werden die out PORTC,???? mit ???? = FORE oder BACK sowie die ori ZL,0x?0 mit ? = 0..7 spezifisch besetzt. Der Font hatte anfangs nur Zeichen 0x08 bis 0x0B welche die Ziffern 0 1 2 3 beinhalten, um diese 10 mal pro Zeile wiederholt auszugeben, dies jede Zeile um ein Zeichen nach links verschoben. Die Ziffern und ihre passenden BACK bzw FORE Muster sind:

........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x00
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x10
()..()..  braucht  FORE BACK FORE BACK  mit  ori ZL,0x20
()..()..  braucht  FORE BACK FORE BACK  mit  ori ZL,0x30
()..()..  braucht  FORE BACK FORE BACK  mit  ori ZL,0x40
()..()..  braucht  FORE BACK FORE BACK  mit  ori ZL,0x50
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x60
........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x70

........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x00
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x10
()()....  braucht  FORE FORE BACK BACK  mit  ori ZL,0x20
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x30
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x40
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x50
()()()..  braucht  FORE FORE FORE BACK  mit  ori ZL,0x60
........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x70

........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x00
()()....  braucht  FORE FORE BACK BACK  mit  ori ZL,0x10
....()..  braucht  BACK BACK FORE BACK  mit  ori ZL,0x20
....()..  braucht  BACK BACK FORE BACK  mit  ori ZL,0x30
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x40
()......  braucht  FORE BACK BACK BACK  mit  ori ZL,0x50
()()()..  braucht  FORE FORE FORE BACK  mit  ori ZL,0x60
........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x70

........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x00
()()....  braucht  FORE FORE BACK BACK  mit  ori ZL,0x10
....()..  braucht  BACK BACK FORE BACK  mit  ori ZL,0x20
..()....  braucht  BACK FORE BACK BACK  mit  ori ZL,0x30
....()..  braucht  BACK BACK FORE BACK  mit  ori ZL,0x40
....()..  braucht  BACK BACK FORE BACK  mit  ori ZL,0x50
()()....  braucht  FORE FORE BACK BACK  mit  ori ZL,0x60
........  braucht  BACK BACK BACK BACK  mit  ori ZL,0x70
    

Identisch ist in Programm selber alles "need fuses", die .include bis .org, die jmp Befehle für Reset und Interrupts, die leere Interrupt Serviceroutine RETI:, sowie den Reset behandeln ab RESET:. Vergleichbar sind die 4 Sync Muster und 8 Grundfarben, hier neu benamst, dann aber wie gehabt in Register gestellt, wenn auch in anderer Reihenfolge, und an Farben trotz mehr definiert nur BACK=blau und FORE=gelb geladen. Port PC vorbereiten ist identisch, ebenso die LOOP: Schleife mit call FRAME drin. Wirklich neu ist hier nur dazwischen mit call INBUF den Bildspeicher aufbauen.

Dieser besteht aus VIDCOLS = 40 Zeichen/Zeile, sowie wegen Zeilenabbruch Pseudozeichen VIDCHARS = VIDCOLS+1 = 41 für Bytes/Zeile, das alles mal VIDROWS = 25 Zeilen. Es fängt bei VIDMEM = MINRAM an, Also sind dies 1025 Bytes am Anfang des AVR SRAMs. Dann wird ABORT = 0x07 definiert, das Zeilenabbruch Pseudozeichen. Dazu FIRST = 0x08 und RANGE = 0x03, um die Zeichen 0x08 bis 0x08+0x03=0x0B ausgeben zu können. In INBUF: selber werden die Register XH:XL auf den Anfang gestellt, und mit 2 verschachtelten Schleifen der Speicher gefüllt. Ab INRLOOP: kommt das erste Zeichen der Zeile bestimmen, mit abwechselnd 0 oder 1 oder 2 oder 3 anfangend, mit FIRST+(VIDROWS-Zeile)&RANGE. Dann ab INCLOOP: VIDCOLS mal mit st X+,AL das Zeichen schreiben und nächstes der 4 Zeichen bestimmen. Gefolgt von am Ende nur einmal das Zeilenabbruch Pseudozeichen.

Ab FRAME: wird das Bild ausgegeben, wieder fast identisch wie bisher, wieder ab jetzt mit Taktzahl in Kommentaren aufsummiert. Statt dem Register TH wird FRLINE benutzt. Alle call VRLINE und call BLLINE wurden je einen Takt kürzer, ohne dem out PORTC,BLK am Ende, was hier mit 1 nop mehr kompensiert wird. Sonst sind sie gleich. Die call WAIT sind identisch. Statt den 80+40*2+80+40*2+80=400 mal call PXLINE/BLLINE/SHLINE gibt es neu aber nur 25 mal call DRROW, welches jedes mal 8*2=16 Zeilen zeichnet für 25*16=400, als 8 Zeilenpaare mit jeweils Font Inhalt im ersten davon bzw ohne/blank im zweiten, bzw genauer 8+7=15 volle Zeilen mit call WAIT, sowie 1 ohne diesem. Sonst ist alles wie gehabt.

Ab DRROW: kommt das entscheidende. Weil für ersten Anlauf, wurde alles so simpel wie möglich auscodiert, egal ob dies den Programmcode unnötig wiederholt. Optimieren kann man ja später. Alles mit horizontalem Syncpuls und Normal bleibt gleich, erst Bild wird anderst. Dazu muss aber wegen in der Zeit "nebenbei" alles vorbereiten, das call WAIT von Normal gekürzt werden, von 58 auf 40+1. Das eigentliche schwarz ausgeben dauert aber gleich lang!

In den so freiwerdenden 58-(40+1)=17 Takten geschieht einiges. Das XH:XL muss für die im Font laufenden ld ZL,X+ auf den aktuellen Zeilenanfang gestellt werden, was auf VIDMEM+(VIDROWS-FRLINE)*VIDCHARS hinausläuft, mit FRLINE = 25..1, und somit (VIDROWS-FRLINE) = 0..24. Danach muss, weil der Font ja stets ein Zeichen ausgibt und dabei "parallel" das nächste vorbereitet, das erste Zeichen vor dieses ausgeben im voraus vorbereitet werden, was am Zeilenanfang das Gegenstück zum Zeilenabbruch Pseudozeichen ergibt, welches nur ausgibt aber nichts mehr vorbereitet. Dazu gibt es eine Kopie der zwischen die 4 out PORTC,???? liegenden 6 Befehle, endend in ihrem ijmp.

Damit kommt insgesammt ohne ausgeben erstes Zeichen vorbereiten, dann 1 ausgeben mit 2 vorbereiten, dann 2 ausgeben mit 3 vorbereiten, hindurch bis 40 ausgeben mit dem "41" vorbereiten, dann Pseudozeichen ausgeben ohne etwas vorbereiten. Das ist die Videopipeline. Dabei laufen die 40 Zeichen ihre Font Routinen ab, was 40*12=480 Takte dauert, und 40*4=160 Pixel ausgibt, aber auch XH:XL um 40 erhöht. Bis es beim 41sten Zeichen zum 0x07 kommt, was zu dessen out PORTC,NONE und jmp DREND0 ausführen führt, und damit wieder zurück zu DRROW bei DREND0: fortsetzen, genau nach dem ijmp der von dort wegging. Dieses muss jetzt die 30 Takte horizontales Normal fertig abwarten. Danach muss es noch das Äquivalent zu BLLINE machen, für eine Leerzeile, da ja wegen 8-zeiligem Font 8 Zeilenpaare von je 1 Font und 1 Leer ausgegeben werden, weil der VGA Monitor 16 Zeilen erwartet. Wiederhole das alles 8 mal, mit im zeichnenden Teil den ori ZL,0x?0 ihren ? = 0..7 auch hier wie im Font angepasst, und somit auch am Ende mit dem letztbenutzten ijmp via dem Pseudozeichen zum DREND?: mit dem richtigen ? = 0..7 zurück. Dabei endet das "eingebaute" BLLINE von letztem DREND7: wieder mit Zeit für nach FRAME zurück.

Damit konnte ich erstmals Textausgabe erzeugen, wenn auch im ersten Anlauf etwas massiv schief lief. Wer die Funktionsweise von Multisync CRT Monitoren versteht, erkennt schnell was hier passiert: Die "Wellen" sind das Resultat von Horizontalfrequenz sich von Zeile zu Zeile ändern, bzw genauer die Zeilenzeit. Zudem das innerhalb einer Textzeile zyklisch klein (regelmässige Welle) und von Rücklauf zu Ausgabe einmal gross (obere Einbuchtung), und der Monitor versucht sich diesem vorzu anzupassen. Genau um solche Effekte klar zu sehen, muss man solche Entwicklung mit einem CRT machen, weil diese 100% volldumme Analogelektronik sind. Mit LCD steigt einem das Teil bei Syncfehler einfach aus und stellt Bild auf schwarz, weil deren halbintelligente Digitalelektronik verwirrt wird.

Genauer ist die grosse "Welle" oben von BLLINE zu DRROW Zeilen länger werdend, und die kleinen "Wellen" innerhalb von DRROW unregelmässig sein. Man merkt aber auch, dass von den 8*2 Zeilen vom Font nur 5*2 dort sind. Analyse der Pixelmuster zeigte, dass genau die Zeilen 3,5,7 von den 0..7 fehlten. Ursache von all dies war nur ein einzelner Programmfehler, die Menge an nop im Zeilenabbruch Pseudozeichen falsch gerechnet, weshalb das Timing der Zeilen jede anderst war, und manche der DREND? gar nicht dran kamen, was dann 3 Zeilen verlor. Das korrigiert ergab fast korrekt mit nur noch einer sehr kleinen "Welle" in den ersten Zeilen eines Zeichens, welche auch von um einen nop danebenliegen herkam. Wie man sieht ist Taktzyklen exakt genau rechnen absolut kritisch, weil dies maximal harte Realtime Programmierung ist.

Programm "VGA Text Static" Version 2

Damit hatte ich zwar Bild, aber mit in DRROW dem Programmcode unnötig 8 mal wiederholt, was unschön ist. Also alles umgebaut als zweite Version. Der Font ist identisch. Auch im Programm ist nur wenig anderst geworden.

Neben VIDCOLS und VIDROWS gibt es neu auch VIDLPR = 16 für Videozeilen pro Zeichenzeile. Das BLLINE: heisst neu VBLINE:, weil nur noch im Rücklauf benutzt, und folglich VRLINE: neu VPLINE:, weil mit Puls. Diese sind beide einen Takt länger, mit wieder einem out PORTC,NONE am Ende, um mit dem neuen CHLINE: kompatibel zu sein, also auch in FRAME: den 1 nop kompensieren wieder weg. Zudem sind noch diverse Kommentare verbessert worden.

Gross anderst ist nur FRAME:, dessen alte Schleife FRDLOOP: welche bisher FRLINE=VIDROWS=25 mal DRROW aufgerufen hat ersetzt wurde, durch 2 verschachtelte Schleifen, FRRLOOP: welche FRROW=VIDROWS=25 mal wiederum FRLLOOP: aufruft welche dann FRLINE=VIDLPR=16 mal CHLINE: aufruft, welches nur noch noch eine Zeile zeichnet, wie es auch VRLINE: und VPLINE: machen, sowie die PXLINE: und SHLINE: vom Testbild Generator bereits gemacht haben. Weit sauberer.

DRROW: ist aber sogar komplett weg und durch CHLINE: ersetzt. Dieses erzeugt wie gehabt alles mit horizontalem Syncpuls und Normal. Letzteres mit dem call WAIT nach vorhin schon von 58 auf 40+1, weiter auf 34+1 gekürzt, mit 23 Takte nun frei. Anfang Videospeicher rechnen ist gleich, nur FRLINE heisst jetzt von oben her FRROW. Aber mangels 8-facher Wiederholung von allem, muss nun für den Einsprung ins richtige Segment in den Fontroutinen das ori ZL,0x?0 sein ? = 0..7 ersetzt werden, durch ein or ZL,AL welches zuerst in AL berechnet werden muss, mit dazu 8*((VIDLPR-FRLINE)&0xFE), was aus Videozeilen 0..15 zuerst 0,0,2,2,4,4,..,14,14 macht, und dann 0,0,8,8,16,16,..,120,120.

Danach ist der restliche Programmcode nur noch wie gehabt, weil der Font ja stets ein Zeichen ausgibt und "parallel" das nächste vorbereitet, auch das erste Zeichen vor dieses ausgeben voraus vorbereiten. Der einzige noch verbleibende Unterschied ist, dass das 0x07 Zeilenabbruch Pseudozeichen jetzt nach seinem out PORTC,NONE ein ret Befehl macht, und damit direkt in FRAME zurückkehrt, kein DREND? mehr braucht.

Damit hatte ich wieder Bild. Aber mit allen 25*16=400 Zeilen gezeichnet, statt 25*8*2=400 nur jedes zweite. Was bei der echten VGA eigentlich so gemacht wird, CGA 200 in 400 anzeigen, was als CGA Doublescan bezeichnet wird. Das Resultat ist aber blockiger. Es hat dort noch eine kleine "Welle", weil in FRAME die äussere Schleife zu lang war, von die beiden Schleifenzähler setzen, was aber nicht kompensiert wurde. Ein häufiges Problem bei solchem zyklenkritisches Zeugs programmieren.

Programm "VGA Text Static" Version 3

Das gefiel mir aber immer noch nicht. Also ging es noch am selbigen Tag weiter zur dritten Version. Der Font ist wieder identisch. Im Programm ist noch weniger anderst geworden. VIDLPR flog wieder raus. Das 0x07 bleibt identisch.

Nur FRAME: ist wieder gross anderst, tut nach 12+25 Zeilen Rücklauf (Bug, das sollten 12+24 sein!) eine weitere mit weniger call WAIT machen, um Zeit für die Vorbereitung zu bekommen, um obigen Fehler korrigieren zu können.

Wenn schon dabei, wird hier aber auch gleich alles andere mit vorbereitet, statt erst in CHLINE:. Statt dort jeweils Framebuffer Zeilenanfang für Zeichen holen in XH:XL laden und rechnen, wird jetzt einmal Frameanfang in YH:YL geladen, und dann für jede Zeile mit movw XH:XL,YH:YL in XH:XL kopiert. Weiter wird AL für Segment in Zeichen einmal auf 0x00 gestellt, und nach jedem Zeilenpaar mit subi AL,-0x10 um 0x10 erhöht und mit andi AL, 0x70 im Bereich gehalten, statt AL neu gerechnet. Dann wird separat das achte Zeilenpaar ausgegeben, ebenfalls wegen call WAIT anpassen, und mit adiw YH:YL,VIDCHARS YH:YL auf die nächste Zeichenzeile korrigiert, statt eben XH:XL neu rechnen. Im Nachhinein erachte ich diese Version als eine Verschlechterung.

Danach ist CHLINE: vereinfacht, nur genau noch horizontalen Syncpuls und Normal, und danach kommt gleich das erste Zeichen vor dieses ausgeben voraus vorbereiten. Womit das Resultat dann vollständig korrekt so aussieht.

Danach folgten diverse Versuche. Einerseits VGA Graphikmodus mit 480/8=30 Zeilen und 60 Bilder/Sekunde, statt Textmodus mit 400/8=25 Zeilen und 70 Bilder/Sekunde. Flackert mir aber zu fest, also nur als Alternative anbieten. Ausserdem ein 5von8 statt 6von8 Videozeilen Font. Der sieht eigentlich sogar besser aus, aber fühlt sich nicht so Retro an, also weiter mit den 6von8. Weiterhin als Farben statt gelb mit cyan und hellblau/pastellblau probiert. Letzteres ist besser, und wurde daher zum Standard von jetzt an.

Programm "VGA Text Static" Version 4

Danach folgte den ganzen Programmcode aufräumen, was in der vierten Version resultierte. Der Font ist wieder identisch. Der Code macht fast genau das selbige, ist aber besser lesbar.

Einerseits mehr und bessere Kommentare, sowie bessere Code Anordnung mit Strichen und mehr Leerzeilen. Dabei auch .equ und .def Zeilen gruppiert ausserhalb von eigentlichen Code. Zudem manche Namen verbessert, wie RETI: zu INTR:, und INBUF: zu FBINIT: (und alle VID* Variablen in IN* Schleifen darin zu FB*, FB steht für FrameBuffer = Bildspeicher). Weiterhin alle Taktzyklenkommentare kompakter aber besser sichtbar als {n} statt n clocks.

Anderseits umgeordnet in sinnvollere Reihenfolge. Mit zuerst alle Hilfsroutinen, wie WAIT: und VPLINE: und VBLINE:, sowie Teile der Initialisierung in mehr Unterprogramme zerlegt, wie RETRINIT: und COLINIT:, und dann erst darauf aufbauendes. DRLINE: ist eine leere Routine, welche nur eine Idee festhielt, aber nicht implementiert wurde, und auch nicht aufgerufen wird, und ab Version 5 ohnehin wieder weg ist.

FRAME: hat beim Bild zeichnen statt zweimal call CHLINE mit CGA Doublescan welches blockig wirkt, nun nur einmal call CHLINE und einmal call VBLINE für altmodischer streifig, wobei die zweimal CHLINE Variante noch auskommentiert drin ist. LOOP: ist jetzt nur die Schleife, ohne Zeugs davor, weil erst danach RESET: alle *INIT Sachen aufruft, vor mit jmp in LOOP übergehen. Die kommentierte Font Routine kommt dann vor dem Spezialfall des Zeilenabbruch Pseudozeichens, nach welchem gleich der Font geladen wird, wie bisher als allerletztes.

Programm "VGA Text Static" Version 5

Nach diesem aufräumen kam wieder Funktionen erweitern, was in der fünften Version resultierte. Dies besteht vor allem aus FRAME: komplett reorganisieren, sowie dem Font erweitern. Aber auch leichtes Kommentare verbessern und Routinen umbenennen, wie VBLINE: wieder zu BLLINE:, weil wieder im Bild zeichnen benutzt, und weil es der universellere Fall ist nun vor VPLINE: angeordnet.

Um mehr verplanbare Zeit in FRAME: zu haben, wurde die bisherige Syncpuls(kurz) - Normal(lang) - Bildzeichnen - Normal(kurz) Reihenfolge ersetzt durch Bildzeichnen - Normal(kurz) - Syncpuls(kurz) - Normal(lang). Damit wird die längere Wartezeit einer Zeile an den Aufrufer zurückgegeben, bzw die noch verbleibende Zeit der letzten Zeile eines Zeilenblockes nach weiter oben, um damit mehr machen zu können. Das erlaubte erst FRAME: aufzuräumen. Es verlangte aber danach, dazu bei jeder Zeilenzeichen bzw Zeilenblock Routine eine "Vorarbeitszeit" Rechnung aufzuführen, und im Aufrufer diese fertig zu rechnen.

Der geplante Ansatz mit DRLINE: wurde dann fallengelassen. Statt dessen wird mit Zeilenblöcken gearbeitet. BLLINE: und VPLINE: und CHLINE: zeichnen alle je eine Zeile, ab ersten Takt von (leeren) Bild, mit nach allen stets 55 Zyklen verbleibend.

Das CHLINE: beinhaltet nun direkt nach dem ijmp die kommentierte Font Zeichenroutine, und gleich danach ebenfalls kommentiert das Zeilenabbruch Pseudozeichen. Letzteres macht nun ein out PORTC,NONE und jmp CHEND (4 Takte). Dies direkt gefolgt vom Code ab CHEND:, weil erst jetzt alles mit dem horizontalem Rücklauf kommt (26von30 Normal, 30 Sync, start Normal, und dann ret mit 55 Zyklen verbleibend). CHLINE: muss aber das erste Zeichen voraus vorbereiten, weshalb es mit "pre" Takten "Vorarbeitszeit" dokumentiert ist, mit 8 dafür, und 4 weitere für Farben weiter unten.

Darauf aufbauend kommt CHPAIR:, welches nur wahlweise zweimal call CHLINE oder einmal call CHLINE und einmal call BLLINE macht, also eine Fontzeile in zwei Videozeilen. Dazu wird der Pin PD6 an Optionen Schalter an Port PD benutzt, mit 1 (= offen) zu BLLINE: gehend, und mit 0 (= auf 0V verbunden) zu CHLINE:. Es wurde dazu genau PD6 genommen, weil dessen Pin auf dem 5x2=10 Pfostenstecker gleich neben dem 0V liegt, um es zu jumpern, da die Hilfsplatine mit 8 DIP Schalter zu der Zeit noch nicht existierte. Da CHLINE: 12 Takte "pre" hat, ist das der "schnelle" Fall, mit für BLLINE: noch ein paar nop kompensieren. Wegen all dies hat CHPAIR: immer noch 55 Zyklen verbleibend, aber nun 4+12=16 "pre" Takte.

Darauf aufbauend kommt ROWDRAW:, welches alle 8 Zeilenpaare von Font zeichnet, und damit auf 16 Videozeilen kommt. Darin kommt die ganze jede Zeile mit movw XH:XL,YH:YL in XH:XL kopiert Logik, sowie das TH für Segment in Zeichen einmal auf 0x00 gestellt, und nach jedem Zeilenpaar 0..6 um 0x10 erhöht werden. Nach Zeilenpaar 7 entfällt letzteres, weil dort nur noch ret kommt. Damit verbleiben noch 51 Zyklen, zudem sind es nun bereits 1+1+1+4+16=23 "pre" Takte.

Darauf aufbauend kommt FRDRAW:, welches alle 25 Zeichenzeilen vom Bild zeichnet, und damit auf 400 Videozeilen kommt. Darin kommt die ganze YH:YL=FBRAM Logik, und dann nur ROWDRAW 24 mal in Schleife, sowie 1 mal am Schluss aufrufen. Alles modular aufgebaut. Mit nun 47 verbleibende und 2+2+4+23=31 "pre".

Die FRBOTTOM: und FRVPULSE: und FRTOP: erzeugen mit call BLLINE bzw call VPLINE aufrufen dann 11+1=12 bzw 12+1=13 bzw 24+1=25 Zeilen vom vertikalen Rücklauf, mit je 51 verbleibende und 1+4=5 "pre". Wonach das eigentliche FRAME: nur noch all diese aufruft, mit jeweils dazwischen die verbleibende und "pre" einbeziehen, und mit selber 47 verbleibende und 4+31=35 "pre" haben. Was dann wiederum LOOP mit einbezieht. Der Rest ab RESET ist wie gehabt.

Dabei lief es immer noch am selbigen Tag wieder im ersten Anlauf etwas schief. Aber der Fehler war diesmal im Wechsel von Bild zu Rücklauf. Was erst erkennbar wurde nachdem ich absichtlich in BLLINE 2/3 intensives orange ausgab, und somit den Rahmen sichtbar machte. Wieder mal etwas wo man unbedingt eine volldumme CRT und kein halbintelligentes LCD haben will. Schuld waren die nop im CHPAIR beim Wechsel von CHLINE zu BLLINE, nicht richtig kompensiert dass letzteres weniger "pre" braucht.

Weiter werden danach auch pro Zeile zwei eigene Farben in BACK und FORE geladen, weshalb der Bildspeicher von 40Zeichen+1Ende=41 auf 2Farben+40Zeichen+1Ende=43 Bytes/Zeile erweitert wurde, und diese mit obigen 4 weiteren "pre" Takten in die CLUT Register geladen. Daher wird FBINIT auch aufwendiger, um diese Farben in jeder Zeile zu setzen, BACK immer blau, FORE cyan mit zyklisch ansteigend rot Anteil darin bis es weiss wird. Wie man dies hier erstmals recht gut sehen konnte.

Dann wurden wegen Font auf volles ASCII erweitern in vga_text_font.inc die bestehenden Zeichen 0 1 2 3 von 0x08..0x0B zu 0x30..0x33 verschoben. Danach wurde auf ganzen ASCII Umfang erweitert. Benutzt wurde ein bereits seit langem designter 4x8 Pixel Font. Ebenso wurde auch das Zeilenabbruch Pseudozeichen von 0x07 auf ASCII 0x7F (Del) verschoben. Deswegen wird nun auch zuerst das .include "vga_text_font.inc" benutzt, und erst danach das Pseudozeichen erzeugt.

Aber da davor bereits 4 Zeichen * 8 Segmente * 16+1 Zeilen Programmcode im vga_text_font.inc waren, wäre dies nun mit 95 Zeichen sehr viel Arbeit geworden, und das erst noch sehr fehleranfällig. Immerhin ist es Programmcode der assembliert (95von96)/128 = 75% von 32k = 24k ergibt! Also habe ich nur die eigentlichen Fontmuster nach vga_text_font.fon extrahiert, in Format von 8 Zeilen mit je 4 Zeichenpaaren mit eines davon pro Pixel, mit .. = BACK und () = FORE. Die erste Zeile CHAR ist kommentiert mit "Zeichen", aa=ASCII (ASCII Code), hh=ASCII/2 A13..8 (High Adresse), ll=ASCII/2 A7 (Low Adresse), cc=Zeichencode A7+0+A13..8 in SRAM (kombnierte hh und ll). Die ersten 4 ASCII Ziffern 0x30 bis 0x33 sehen in der vga_text_font.fon Fontdatei dann so aus:

CHAR "0", aa=0x30, hh=0x18, ll=0x00, cc=0x18
........
..()....
()..()..
()..()..
()..()..
()..()..
..()....
........

CHAR "1", aa=0x31, hh=0x18, ll=0x80, cc=0x98
........
..()....
()()....
..()....
..()....
..()....
()()()..
........

CHAR "2", aa=0x32, hh=0x19, ll=0x00, cc=0x19
........
()()....
....()..
....()..
..()....
()......
()()()..
........

CHAR "3", aa=0x33, hh=0x19, ll=0x80, cc=0x89
........
()()....
....()..
..()....
....()..
....()..
()()....
........
    

Danach wurde ein Shellscript Hilfprogamm namens genfont geschrieben, welches von diesem ausgehend das passende vga_text_font.inc erzeugt, aus 11k Font "Source" fast 150k Assembler Code macht. Wegen all dies aufrufen, wurde auch das Makefile erweitert.

Wegen weit mehr Font, und diesem als ASCII angeordnet, gibt es weitere Änderungen im Programm. Neu ist FBWCHAR:, welches alles kleiner 0x20 (Space) oder grössergleich 0x7F (Del) mit 0x2E (Punkt) substituiert (die selbige Konvention wie in Hexdumps und Debugger wenn ASCII neben Hex ausgegeben wird), gefolgt von ASCII Bits passend zu der im 8bit Zeichencode benutzten Anordnung umformen, mit Bit7 ins T Flag retten (bst AL,0) und durch 2 teilen (lsr AL) womit ASCII Bits7..1 zu Zeichencode Bits6..0 werden und Bit von T holen (bld AL,7) womit ASCII Bit0 zu Zeichencode Bit7 wird, gefolgt von im Bildspeicher ablegen.

Das FBINIT benutzt nun dieses, und zeigt damit statt zyklisch die alten 4 Zeichen nun die neuen, anfangs 64 und dann 95, wieder mit pro Zeile um eines verschoben anfangen. Ausnahme ist um das 0x7F selber zu schreiben, welches FBWCHAR genau verhindern würde, also muss FBINIT dessen cc=0xBF direkt ablegen. Das Resultat sah mit ASCII 0x20..0x5F bereits so aus, und danach mit vollen ASCII 0x20..0x7E sogar so aus, mit nach Zeile 12 um 31 mehr nach links verschieben.

Programm "VGA Text Static" Final

Damit stand die Ausgabe, aber das mit FBINIT: aufgebaute Bild war langweilig und nutzte die Farben nicht wirklich aus. Um besseres zu machen wurde das Bild generieren von FBINIT: getrennt, was in der sechsten und finalen Version resultierte.

Dazu wurde als erstes nach WAIT: und vor RETRINIT: ein kleines Stringpaket addiert. Dieses hat einem einzelnen String von 80 Zeichen, explizit in .dseg alloziert, mit STINIT: um dieses auf 0 von 80 zu setzen, und STLDI: um es mit Inhalt aus dem Programm zu füllen. Das STLDI: wird aufgerufen mit call STLDI gefolgt von .dw Länge und .db "Zeichen". Es muss daher die Folgeaddresse im aufrufenden Programm vom Stack holen mit pop ZH und pop TL, dann mit lsl und rol von Programm Wortadresse auf Daten Byteadresse korrigiert werden, dann kann mit lpm die Läge nach TH:TL gelesen werden, und mit lpm und st die Zeichen in den String umkopiert werden.

Am bestehenden Programmcode blieb vieles gleich oder sehr ähnlich. BLLINE: und VPLINE: sind identisch. Der Bildschirmspeicher bekam statt fest FBROWLEN = 43 neu FBROWLEN = FBCOLOURS+FBCOLUMNS+FBROWEND, und daraus entsteht FBSIZE = FBROWS*FBROWLEN+FBEND, um dieses ebenfalls explizit in .dseg zu allozieren. Neben FBWCHAR: gibt es FBWCOLOUR: um die Farben zu setzen, welches explizit andi AL,0x3F macht um die Syncsignale stören zu verhindern. FBWCHAR: selber wurde von alles kleiner 0x20 zu Punkt machen zuerst auf 0x10 erweitert, später auf 0x08. Dazu kommt noch FBWSTR: um obigen String mit call FBWCHAR ins Bild zu kopieren, mitsammt Linewrap und auch Framewrap. Ansonsten wird FBINIT: reduziert, dass es nur noch mit Punkten füllt, weil Bildinhalt danach separat erzeugt wird, sowie nach allen Zeilen ein 0xFF addiert um Framewrap zu erkennen. Neu sind noch XYWCOLOUR: und XYWCHAR: und XYWSTR: um in den Bildspeicher zu schreiben. Dabei lief wieder im ersten Anlauf etwas schief. Es war ein Rechenfehler weil add und subi #-1 mit einander inkompatible Carryflag Nutzung haben, was ich bisher nicht bemerkt hatte.

Die CHLINE: und CHPAIR: blieben anfangs identisch, aber ROWDRAW: und FRDRAW: und FRBOTTOM: und FRVPULSE: und FRTOP: und FRAME: bekamen alle, statt dem letzten call+ret, direkt ein nop+jmp. Womit der mehrfache ret entfällt, sie alle konstant 55 Zyklen verbleibend haben, was die Wartezeit Rechnungen vereinfacht bzw vereinheitlicht. LOOP: ist gleich, heisst aber neu besser DISPLAY:.

Neu ist nun separat das eigentliche Bild erstellen. DEBLANK: erzeugt eine Leerzeile, setzt dazu mit call XYWCOLOUR Hintergrund schwarz und schreibt mit call XYWSTR 40 Leerzeichen. DEMO: baut ein ganzes Bild auf, aus 3 Zeilen ASCII 0x20..0x3F 0x40..0x5F und 0x60..0x7E, dann Leerzeile, dann 6 Zeilen Musterlinie mit 6 Hintergrundfarben und Textfarbe stets schwarz, wieder Leerzeile, dann 3 Zeilen Projekteigenschaften beschreiben, wieder Leerzeile, dann 7 Zeilen Graphikeffekte mit 7 Textfarben und Hintergrund stets schwarz, wieder Leerzeile, dann 2 Zeilen Projektinfo.

Danach wird noch RESET: erweitert, mit call STINIT vor call RETRINIT und call FBINIT, sowie call DEMO zwischen call FBINIT und call DISPLAY. Der Zeichensatz wurde von ASCII 0x20..0x7E noch erweitert mit p-A 0x10..0x1F (p-A = pseudo-ASCII), welche aber alle damals auf Leerzeichen gesetzt wurden, erstaunlicherwiese nicht auf die 16 Blockgraphik Muster. Das Resultat von all diesem ist bereits weit farbiger.

Aber das liess sich noch verbessern. Dazu wurde FBCOLOURS von 2 auf 5 vergrössert. CHLINE: wurde erweitert um neben BACK und FORE auch CLUT Register ALT1 bis ALT3 zu laden, zudem wurde TEMP definiert. Daher musste auch in CHPAIR: der call WAIT korrigiert werden. Am wichtigsten ist aber, vor den Fontzeichen 0x10..0x1F von vga_text_font.inc noch Spezialzeichen 0x08..0x0F von vga_text_specials.inc zu laden. Daher wurde FBWCHAR: erneut erweitert, von alles kleiner 0x10 auf 0x08.

Diese Spezialzeichen geben alle ein Leerzeichen aus, aber haben nur ein einzelnes der 4 out PORTC,BACK drin, was 3 Takte für Programmcode freimacht um Effekte zu erzeugen. Diese reichen genau um einen Satz von dreimal mov zu benutzen, jeweils TEMP=etwas, etwas=anderes, anderes=TEMP, womit man Farben austauschen kann. 0x08 und 0x09 tauschen FORE und BACK, für reverse ein und aus (der Unterschied ist, dass "ein" alles BACK ausgibt, aber "aus" alles FORE weil es das "(noch-)reverses" BACK ist), 0x0A und 0x0B machen dies nur in Zeile 7 vom Font, für unterstrichen ein und aus, 0x0C und 0x0D und 0x0E tauschen FORE gegen ALT1 bis ALT3 (mit stets BACK ausgeben), 0x0F ist unbenutzt.

(Dieser Zeichencode benutzen um Attribute zu modifizieren während als Leerzeichen ausgeben Trick, ist mir erstmals in den 1980ern in einem c't Eigenbau Terminal Projekt begegnet (mit 0..127 = 128 Zeichen, und 128..255 setzen 7 Attribute inklusive einem davon revers video). Erst nach dem 2016er Video als Medium Vortrag habe ich herausgefunden, dass die Oric Rechner dies auch benutzt haben, und habe diesen deswegen dort nachgetragen.)

Danach wurden in DEMO die 7 Zeilen an Graphikeffekten durch all dies benutzenden Text ersetzt. Dabei "nor" (= normal) in FORE, "inv" (=invers) in revers, "nor" in FORE, "ulin" (= underline) unterstichen, "nor" in FORE, "2/3" in ALT1, "nor" in FORE, "1/3" in ALT2, "nor" in FORE, "pas" (= pastell) in ALT3. Man beachte, dass die Spezialzeichen stets ein Leerzeichen an Platz verbrauchen, ebenso dass sie nur "nor" zu etwas oder dieses etwas zurück zu "nor" schalten können, niemals direkt von einem etwas zu etwas anderes. Das Resultat davon ist noch am selbigen Tag nochmals farbiger geworden. Das ist die letzte Evolutionsstufe dieser Programmserie.

Threaded "Direct"

Damit hat es zwar ein ordentliches Bild, aber das ganze bisher ist mit einem massiven Problem behaftet. Dieses besteht darin, dass nach allem initialisieren (mit STRINIT: FBINIT: SYNCINIT: CHINIT:), das Bild zuerst vollständig aufgebaut werden muss (mit DEMO:), und erst dann mit ausgeben angefangen wird (mit DISPLAY:), wobei das Bild nicht mehr verändet werden kann. Das ist so, weil ab DISPLAY: starten 100% der Prozessorleistung dort mit anzeigen verbraucht wird, und ausser dem einen Prozessor nichts auf den Videospeicher im SRAM zugreifen kann. Daher hiessen alle Programmversionen bisher "VGA Static" (der Testbild Generator) bzw "VGA Text Static" (die Textausgabe), weil stets ein statisches Bild erzeugend. Animation ist damit aber nicht machbar.

Dem kann aber abgeholfen werden, durch den Prozessor teilen, zwischen aufbauen und danach modifizieren, sowie ausgeben. Was auf Multithreading hinausläuft, bei dem man den Prozessor dazu bringen muss, so benutzbar zu sein, wie wenn es mehrere davon hätte. Wenn auch hier in einer sehr simplen Form davon, mit nur genau 2 Threads, und diese zudem in einer festen Prioritätsordnung, von Vordergrundthread Bild anzeigen vor Hintergrundthread Bild aufbauen und modifizieren. Dabei ist während in einer Zeile zeichen der Prozessor zu 100% am ausgeben, aber im horizontalen und vertikalen Rücklauf oft in WAIT am warten. Man merkt dabei bald, dass der horizontale nur kurze 2-stellige Anzahl Zyklen wartet, aber der vertikale in einer 2-stelligen Anzahl Leerzeilen je eine 3-stellige Anzahl Zyklen wartet. Also sind letztere der Ort wo ein Hintergrundthread den Prozessor bekommen kann. Und netterweise ist der vertikale Rücklauf auch der optimale Zeitpunkt um zu animieren.

Jedes einzelne Programm so zu schreiben, dass es den Prozessor zeilenweise bekommt, dort drin Zyklen abzählen muss, und stets genau auf Zyklus zurückgeben, wäre extrem mühsam und fehleranfällig. Zum Glück ist aber präemptives Threading eine Standardtechnik in vielen Steuerungen. Weshalb bereits Mikroprozessoren dafür Spezialhardware haben, in Form von Interrupts, welche von einem externen Signal namens Interrupt Request angeworfene Unterprogramme namens Interrupt Serviceroutinen sind, welche "zwischen" beliebigen Befehlen des Hauptprogrammes "eingeschoben" werden. Dazu kamen dedizierte Counter/Timer Chips, um diese regelmässig auszulösen nach n Signaltakten. Daher haben Mikrocontroller solche bereits eingebaut, mit n externen Signaltakten oder auch internen Prozessortaktzyklen benutzbar. Der AVR ist hier keine Ausnahme, sondern hat sogar drei davon drin, Timer/Counter0 8bit, Timer/Counter1 sogar 16bit, Timer/Counter2 8bit mit asynchron, plus weitere dedizierte für die SPI und UART und I2C/TWI Bitraten erzeugen.

Um solches Multithreading zu machen, gibt es einen Standardansatz. Dass einfach der Hintergrundthread (und damit die meisten "normalen" Programme) den Prozessor stets als Hauptprogramm hat, wenn er vom Vordergrundthread unbenutzt ist, und damit umgeht wie wenn er ihn alleine hätte (was alle diese Programme vereinfacht), bis auf damit rechnen müssen dass er den Prozessor wiederholt eine Weile "verliert", was auf ihn nur wirkt wie wenn der Prozessor eine Weile still steht. Aber der Vordergrundthread per Interrupt den Prozessor jederzeit nach Bedarf in seine Interrupt Serviceroutine Unterprogramm "ausleihen" kann, mit bei nicht mehr Bedarf wieder "abgeben", weil der Timer immer wieder passend zum Bedarf einen Interrupt erzeugen wird.

Da je nach ob in Bild oder Rücklauf eine variable Anordnung von Leerzeilen (allenfalls jede zweite, bzw alle) vorhanden ist, ist der beste Ansatz den Timer auf Interrupt einmal pro Zeile einzustellen. Da wir pro Zeile 600 Taktzyklen haben, sieht es auf den ersten Blick danach aus, dass nur der 16bit Timer/Counter1 ausreicht. Aber die AVR Timer/Counter sind sehr flexibel, haben neben ihren Bits auch noch einen Prescaler, der die einkommende Taktfrequenz zuerst /1 oder /8 oder /64 oder /256 oder /1024 teilen kann (neben ausgeschaltet, und im Countermodus an Pins externe 0er oder 1er zählen). Daher reicht hier der Timer/Counter0, mit Prescaler auf /8 und in 8bit auf 600/8=75 laufend. Damit bleibt der leistungsfähigere Timer/Counter1 für den Hintergrundthread frei, ebenso der flexiblere Timer/Counter2, wenn auch letzterer wegen der VGA Hilfsplatine an Port PC angesteckt ohnehin seinen asynchron Modus verliert.

Dazu muss aber der Vordergrundthread speziell geschrieben sein, um mit solchen "Portionen" auszukommen. Insbesondere muss er dazu als Interrupt Serviceroutine geschrieben sein. Hauptunterschied einer solchen ist, dass sie den Prozessor nur temporär bekommt, und danach in genau selbigem Zustand an den ahnungslosen Hintergrundthread abgeben muss, damit dieser nicht verwirrt wird. Neben alle benutzten Register sichern und restaurieren (oft auf dem Stack), muss auch der Stack am selbigen Ort gelassen werden ohne addierte oder entfernte Elemente.

Was zwar erlaubt innerhalb von einem Interruptaufruf Unterprogramme aufzurufen, aber es unmöglich macht zwischen Aufrufen die Position in der Bildlogik in Form von Unterprogrammaufrufen zu verwalten. Der ganze Ansatz von Version 5, mit FRAME: zu FRDRAW: FRBOTTOM: FRVPULSE: FRTOP:, und FRDRAW: zu ROWDRAW: zu CHPAIR:, und alle zu CHLINE: bzw BLLINE: oder VPLINE:, ist damit komplett unmöglich geworden, und muss ersetzt werden. Daraus entstand der zweite Ansatz "VGA Threaded" des Algorithmuses "Direct".

Programm "VGA Threaded"

Aufbauend auf diesen Algorithmus gab es 10 Programmversionen:

Programm "VGA Threaded" Version 1

Diese Version hat noch gar kein Threading drin, und ist eigentlich wie in obiger Programm Version 4 nur einmal kräftig den Programmcode aufräumen, was in der ersten Version hier resultierte. Das Programm ist neu in der Datei vga_threaded.asm, der Font dazu weiterhin in vga_text_font.inc. (Es hätte im Nachhinein wohl besser als Programm "VGA Text Static" Final abgespeichert werden sollen, mit obigem Final als Version 6.)

Als erstes wurden meine Registernamen MH:ML, TH:TL, AH:AL, XH:XL, YH:YL, ZH:ZL aus der ATMega32 Definition m32def.inc herausgenommen und separat in avr_registers.inc abgelegt. Sie wurden dabei massiv erweitert, auf alle 32 Register mit Namen versehen, womit weit mehr davon einfach nutzbar wurden. TH:TL wurde zu Duplikatnamen von ZH:ZL (und ab Version 2 eliminiert weil das sich als eine massive Fehlerquelle erwies). AH:AL bekam Duplikatnamen DH:DL (was in Version 2 ebenfalls eliminiert wurde, aber in Version 7 den Namen ersetzte). All diese Registerpaare sind nur für 16bit Rechnungen reserviert. Für 8bit werden alle anderen 32-6*2=20 Register benamst, in Gruppen, mit C0..C3 Counter (Zähler), A0..A3 Akkumulator, T0..T3 Temporary, I0..I3 Interrupt, G0..G3 Global, S0..S3 Special. Mit bei den Sorten C,A,T,I die 0 und 1 in R16..31 (mit Immediate Modus möglich), alle anderen in R0..15 (ohne Immediate).

(Ab Version 2 wurden S0..S3 zu G4..G7. Diese Aufteilung ist von der offiziellen MIPS Registerkonvention inspiriert. Ab Version 7 verschwanden alle C, und die A wurden zu S (Saved), mit neu T0..T2 Temporary, S0..S2 Saved, I0..I1 Interrupt, alle mit Immediate, sowie einfach V0..V13 für Variablen ohne Immediate. Später wurden letztere zu F0..F13 Fast (Schnelle). Dies ergibt meine bis heute empfohlene AVR Registerkonvention.)

Dann folgen diverse besser lesbar machende Änderungen. Alle Portbelegungen kamen zusammen an den Anfang, als Hardwarebeschreibung. INTR: heisst neu UNUINT: (UNUsed INTerrupt). Es folgen ein kleiner Satz von Hilfsroutinen DBGLED*:, um zwecks Debugging eine LED am RS232 Tester am UART TxD Pin ein-/aus-/umzuschalten, addiert weil ich mit diesen eine Endlosschleife in FBWSTR: suchen musste. WAIT: ist das selbige wie bisher, nur mit Register T0 statt TL, ebenso alle Aufrufe davon. Und der Kommentar "min 1*3-1+4=11 clocks for TL=1" sollte eigentlich rechnerisch =6 und nicht =11 sein. Aber das liegt daran dass der Kommentar umvollständig ist, weil dort noch die 1+4+ davor fehlen, welche die aufrufenden ldi TL,n und call WAIT dazu addieren, was dann 1+4+(1*3-1+4=6)=11 gibt, und nun so vervollständigt wurde.

Dazu kamen an manchen Stellen auch leichte Verbesserungen. Die Stringpaket ST*: werden zu STR*:, und benutzen nur noch ld/st statt lds/sts, arbeiten in T0 statt AL, zählen in AH:AL statt TH:TL, enden mit ijmp statt push+push+ret. Die Syncpuls Muster kommen in die G0..G3 Register. BLLINE: und VPLINE: und CHLINE: benutzen statt out PORTC,etwas neu out VGASPORT,etwas (mit S = Sync) von der Hardwarebeschreibung her, womit die Umsteckbarkeit der Hilfsplatine an beliebige Ports auch in der Software erscheint. Der Font ist identisch, nur mit in genfont ebenfalls auf out VGADPORT,farbe (mit D = DAC) statt out PORTC,farbe umgestellt.

Selbst kleine Bugs wurden entdeckt und korrigiert. Konstante FBCOLOURS heisst neu FBROWBEG als Gegenstück zu FBROWEND. FBWCOLOUR: und FBWCHAR: nutzen wie WAIT neu T0. FBWCOLOUR: testet neu VGAWHITE von der Hardwarebeschreibung statt fest 0x3F. FBWSTR: hat nun korrekt adiw XH:XL,FBROWBEG statt bisher 2 mal ld AL,X+, was seit auf 5 Farben erweitern ein unerkannter Fehler war, und die obige Endlosschleife erzeugte. FBINIT: benutzt C1,C0 statt TH,TL. Die Farbregister werden herumgeschoben zu I1..I3 und S0..S3. Alles von CHLINE: bis FRAME: zeichnen ist identisch, ebenso alles zu DEMO: und RESET:.

Programm "VGA Threaded" Version 2

Auch diese Version hat noch gar kein Threading drin, nur diverse Programmcode Verbesserungen und auch leichte Fehlerkorrekturen, was in der zweiten Version hier resultierte. (Es hätte ebenfalls im Nachhinein besser als Programm "VGA Text Static" Final abgespeichert werden sollen, mit obigem Final als Version 7.)

Wie in Version 1 bereits erwähnt, wurden bei den Registernamen TH:TL und DH:DL eliminiert, sowie S0..S3 zu G4..G7. Die Reihenfolge von Routinen wurde leicht revidiert, mit allem timing spezifischem WAIT:, SYNCINIT:, BLLINE: und VPLINE: erst nach allem geradeaus STR*: und FB*: und XYW*:. Vor allem aber wurde eher der Code verbessert. Als erstes einen Fehler in CHINIT: gefunden, dass sbi MODPIN,MODBIT eigentlich sbi MODPORT,MODBIT sein sollte. Dabei auch gemerkt dass in CHPAIR: sbis PIND,PIND6 konsequent sbis MODPIN,MODBIT sein sollte. Dann wurde in den Spezialzeichen das unbenutzte 0x0F mit Code gefüllt, der statt 3 mov 3 nop macht, damit ein Aufruf kein Absturz erzeugt. Endlich wurden im Font die Zeichen p-A 0x10..0x1F mit Blockgraphik gefüllt, mit Blöcke codiert als oben/links Bit0, oben/rechts Bit1, unten/links Bit2, und unten/rechts Bit3. Zudem wurde in DEMO: die Frequenzangabe von 18.342MHz auf 18.432MHz korrigiert. Ebenso die 6 bzw 7 mal wiederholten Zeilen mit nur anderen Farben in zwei Unterprogramme DERAIN: und DERGB: ausgelagert.

Weit sichtbarerer Unterschied besteht darin, mit der neuen Blockgraphik ein SoftVGA Logo in DEMO: addiert zu haben. Dazu wurden die oberen 6 Zeilen Musterlinie von 40 auf 23 Zeichen verschmälert, um 1 Spalte Trenner (in DESEPAR:) und 16 Spalten Logo (in DELOGO:) unterzubringen. Dabei wurde auch gleich in STRLDI: ein Fehler gefunden, dass bei einer ungeraden Anzahl von Zeichen im wortweise gelesenen Programmspeicher ein Füllzeichen addiert wird, welches nun übersprungen werden sollte, sonst wird letztes Zeichen+Füller als Befehl ausgeführt. Das Resultat davon ist nochmals detaillierter geworden.

Der bedeutsamste Unterschied ist aber intern. Nach schon zwecks Debugging für die LED am UART TxD ansteuern die Befehle cbi und sbi benutzt zu haben, wurde jetzt auch die ganze Syncpulsgenerierung auf cbi und sbi benutzen umgestellt. Den Unterschied sieht man in SYNCINIT:, ebenso BLLINE: und VPLINE: sowie CHLINE: nach CHEND:. Dabei wurden auch die vier Register mit NONE,HOR,VER,H_V Werten überflüssig, und damit für andere Anwendung frei.

Programm "VGA Threaded" Version 3

Nun wurde es Zeit, endlich an die Umstellung auf Threading loszugehen. Da mit Interrupts der ganze FRAME: mit FRDRAW: FRBOTTOM: FRVPULSE: FRTOP: Ansatz verschachtelter Unterprogramme nicht gehen kann, muss der ganze zeichnende Programmcode zuerst in eine mit Interrupt Serviceroutinen kompatible Form umgewandelt werden. Daher ist in der dritten Version alles ausser leichten Kommentaränderungen bis nach SYNCINIT: identisch, neues ist erst ab HPULSE:.

Der neue Code folgt dem Prinzip einer Statemachine. Dabei sind alle beteiligten Zeilensorten Routinen auf der gleichen Unterprogrammebene, machen die für ihren Programmzustand passende Aktion, mit danach in den nächstpassenden Zustand "übergeben" durch sich gegenseitig in der passenden Reihenfolge anspringen, hin und her, bzw sich selber wiederholen wenn das passender ist. Hier ist die passende Aktion die jeweilige Zeilensorte zeichnen aus den dazu passenden Daten, und das gegenseitig sich anspringen gescheiht im horizontalen Rücklauf. Später wird das anfangs der Interrupt Serviceroutine geschehen. Weil dies ein sehr grosser Umbau ist wurde er in fünf Schritten gemacht, und diese alle separat abgespeichert als Versionen 3 bis 7.

Als Zeitablauf im Rücklauf gilt neu in Normal(kurz) aufräumen, im Syncpuls(kurz) warten, und im Normal(lang) springen, sowie im neuen State im "pre" vorbereiten. Später wird alles Interrupt verlassen bzw anfangen im Normal(lang) landen. Die neue "Endroutine" HPULSE: erzeugt mit sbi VGAHPORT,VGAHBIT den Anfang vom Syncpuls, wartet dann, und erzeugt mit cbi VGAHPORT,VGAHBIT dessen Ende und geht dann mit ret zurück, um Normal(lang) als "pre" nutzbar zu machen. Alle Zeilenzeichen Routinen enden mit jmp HPULSE was duplikaten Puls Programmcode beseitigt.

Das umgebaute BLLINE: setzt erst im "Bilddteil" vom Zeitablauf mit cbi VGAVPORT,VGAVBIT auf ohne Vertikalsyncpuls, für falls es das erste BLLINE: nach einem VPLINE: ist, und kompensiert das mit weniger lang warten. Das umgebaute VPLINE: hat nur sbi VGAVPORT,VGAVBIT auf mit Vertikalsyncpuls, für falls es das erste VPLINE: nach einem BLLINE: ist. Bei nicht-ersten Ablauf dieser werden die cbi und sbi einfach zu wiederholt bestätigen. Damit sind alle Pulse erledigt und dazu sehr fehlertolerant und stabil.

Das CHINIT: ist wieder identisch, erst CHLINE: musste umgebaut werden. Dabei ist vorbereiten und Font immer noch identisch, nur das Zeilenabbruch Pseudozeichen ist anderst. Weil HPULSE: existiert, muss es nur noch nach out VGADPORT,BLANK kurz warten und dann mit jmp HPULSE übergehen. Was alles direkt im Pseudozeichen seine 16 Befehle hineinpasst, und damit jmp CHEND überflüssig macht. Ab hier sind CHPAIR: und alles folgende in diesem Schritt identisch, immer noch als verschachtelte Unterprogramme.

Programm "VGA Threaded" Version 4

Danach ging es am selbigen Tag gleich weiter mit dem zweiten Schritt, zur vierten Version, welche bis CHINIT: identisch ist. Dort erscheint eine neue Variable SEGOFF, welche die Font Segmente 0x00..0x70 durchlaufen wird. Sie wird hier auf 0x00 gestellt, sowie in CHLINE nach für or ZL,SEGOFF benutzt werden um 0x10 erhöht. Was bisher alles eine lokale Variable T1 in ROWDRAW: war, welche aber wegen Umbau zu Interrupt Serviceroutinen verschwinden muss, weil lokale Variablen ein Interruptende plus Hintergrundthread plus Interruptanfang nicht überleben.

Für die zweite Zeile eines Zeilenpaares gibt es neu CHLINE2:. Darin wird obiges SEGOFF um 0x10 erhöhen reversiert, für falls zeichnen wieder CHLINE: aufrufen können. Sonst wird das bei CHLINE: erwartete XH:XL+FBROWLEN simuliert und das reversierte erhöhen wieder ent-reversiert, bevor dann mit BLLINE: zeichnen. CHPAIR: heisst neu PAIRDRAW:, benutzt einmal CHLINE: und reversiert dann XH:XL-FBROWLEN falls zeichnen, bevor es CHLINE2: aufruft welches zeichnet oder mit XH:XL+FBROWLEN kompensiert. Diese Reversiererei ist unfreundlich und fehleranfällig, wird daher in der nächsten Version verschwinden.

In ROWDRAW: sind ldi T1,0x00 sowie um 0x10 erhöhen weg, weil CHINIT: und CHLINE: das bereits mit SEGOFF machen. Ansonsten ist das movw XH:XL,YH:YL weg, mitsammt allem YH:YL benutzen. Statt dessen wird es bereits in XH:XL erwartet, und dieses für alle Wiederholungen mit XH:XL-FBROWLEN reversiert, aber nicht nach dem achten Zeilenpaar ausgeben, womit es automatisch für das nächste Zeilenpaar passend bleibt. FRDRAW: lädt daher direkt XH:XL statt YH:YL. Wieder 2 Register frei für andere Anwendung. Ab hier sind FRBOTTOM: und alles folgende in diesem Schritt identisch.

Programm "VGA Threaded" Version 5

Jetzt geht es gross los! So vorbereitet wurde es nun Zeit, mit der eigentlichen Statemachine aufbauen loszulegen, was zur fünften Version führte, welche bis zu HPULSE: identisch ist. Dieses und alle anderen Zeichenroutinen müssen neu ohne die ganzen verschachtelten call/ret laufen, in Vorbereitung auf zu Interrupt Serviceroutinen werden. Statt dessen wird in ZH:ZL die Adresse der Fortsetzung gespeichert, welche von HPULSE: direkt mit ijmp statt ret angesprungen wird, und sp&aul;ter in der Interrupt Serviceroutine verzweigen wird.

Danach wird DISPLAY: total anderst, weil es direkt ohne mehrere Ebenen von call Aufrufen in VRBBSTATE: hineinläuft. Dieses ersetzt die untere Schleife mit BLLINE, durch setzen von VRBBLINE: (Vertical Retrace Bottom Blank LINE) als aktive Fortsetzung, sowie C0=12 Zeilen, und geht via jmp HPULSE und dessen ijmp statt ret zu VRBBLINE:, welches eine Leerzeile ausgibt, sowie C0 um -1 reduziert, mit bei grösser 0 alles via jmp HPULSE wiederholen, aber bei 0 dann zu VRPSTATE: übergeht.

Dieses setzt VRPLINE: (Vertical Retrace Pulse LINE) als Fortsetzung, sowie C0=13 Zeilen, aber startet auch den Vertikalpuls, was alles VPLINE: ersetzt. Rest dann wie gehabt mit jmp HPULSE, mit wiederholen bis bei 0 zu VRTBSTATE: übergehen. Welches die oberen BLLINE: ersetzt, weil zwar unten und oben beide selbiges ausgeben aber nun andere States sind (dies entspricht bisher FRBOTTOM: und FRTOP: Subroutinen welche beide BLLINE: benutzten). Dieses setzt VRTBLINE: (Vertical Retrace Top Blank LINE) als Fortsetzung, sowie C0=25 (erster Unterschied), aber beendet auch den Vertikalpuls (zweiter Unterschied), mit wiederholen bis bei 0 zu CHSTATE: übergehen (dritter Unterschied), welches CHLINE: als Fortsetzung setzt.

Dieses wird am Ende wieder VRBBSTATE: aufrufen, und somit durch alle 4 Sorten Zeilen 12+13+25+400 hindurchlaufen ohne jegliche call/ret zu benutzen, mit nur jeder State ihre Anzahl Zeilen abzählen, was FRAME: ersetzt. Dies zudem endlos wiederholt, was zugleich DISPLAY: ersetzt.

CHINIT: ist fast identisch, nur SEGOFF wird weder definiert noch mit 0x00 geladen. Danach kommt aber oben aufgerufenes CHSTATE:, welches CHLINE: als aktive Fortsetzung setzt, und dann statt nur C0= die ganze Bildausgabe vorbereitet. Hier statt in FRDRAW: bekommen XH:XL ihre Adresse. Ebenso bekommt C0 (statt bisher FRROW) sein FBROWS=25. Auch hier statt in CHINIT: bekommt T1 (statt bisher SEGOFF) sein 0x00. Dazu bekommt noch C1 eine 0x00 um die Zeilenpaare abzuwechseln. Dann wird wie gehabt via jmp HPULSE und dessen ijmp zu CHLINE: gegangen, welches alles zeichnet. Welches dabei auch CHLINE2: und PAIRDRAW: und ROWDRAW: und FRDRAW: ersetzen muss, mit davon auch recht komplex werden.

CHLINE: muss als erstes dem PAIRDRAW: seinen Ablauf von erste/zweite Zeile eines Paares, und als zweites dem CHLINE2: sein Test auf gezeichnete oder leere Zeile entscheiden, mit bei C1=0x00 erstes zeichnen (mit rjmp CHFIRST) und bei C1=0xFF zweites je nach Optionen Schalter an Port PD Pin PD6 auch zeichnen (mit rjmp CHDOUBLE) oder leerlassen (gleich hier bis zum jmp CHEND). Der Unterschied von CHFIRST: zu CHDOUBLE: ist nur 2 Takte mit nop verbrauchen, um den Test zu kompensieren. Nach restliche Horizontalpuls Zeit warten geht es wie gehabt wie beim "alten" CHLINE: weiter, mit den 5 Farben laden und dann das erste Zeichen vorbereiten, gefolgt von mit ijmp ausgeben, und dann die 40*12 Zyklen zeichnen.

Nur das Zeilenabbruch Pseudozeichen beim 41sten Zeichen nach allen wird anders, weil es jetzt die ganze Fortsetzungslogik beinhaltet. Daher ist es wieder mit nur dem out VGADPORT,BLANK und einem jmp CHEND gemacht, weil sonst zu gross. Als erstes wird C1 komplementiert (alle Bits 0 zu 1 bzw 1 zu 0), um zwischen erster und zweiter Zeile im Paar zu unterscheiden, was PAIRDRAW ersetzt. Falls C1 neu 0x00 ist (also erste Zeile in neuem Paar), wird T1 fortgeschritten, um zwischen den 8 Zeilenparen im Zeichen zu unterscheiden, was ROWDRAW: ersetzt. Falls T1 neu 0x00 ist (also erste Zeile in neuer Zeichenzeile), wird C0 reduziert, um zwischen den 25 Zeilen im Bild zu unterscheiden, was FRDRAW: ersetzt. Alles endet sonst wie gehabt mit jmp HPULSE und dessen ijmp zu CHLINE:, ausser C0 wurde zu 0, was dann wie gehabt zu VRBBSTATE: geht für das nächste Bild. Alles nach hier bleibt gleich.

Diese Statemachine Logik ist zwar etwas komplexer, aber korrekt und robust. Trotzdem hatte es jetzt einen massiven Fehler drin, weil der AVR eine saftige Designlimite hat. Der ijmp Befehl kann ausschliesslich mit dem ZH:ZL Registerpaar arbeiten, keinem anderen, das Missfeature schlechthin im AVR. Also benutzen die Segmentzeichenroutinen dieses. Ebenso aber diese Statemachine. Was kollidiert, weil im ersten Durchlauf von CHLINE die Zeichnerei die Statemachine zerdeppert, genauer ZH:ZL von CHLINE auf das Zeilenabbruch Pseudozeichen stehen gelassen wird. Weshalb nach dem HPULSE dessen ijmp gleich wieder zum Pseudozeichen geht, statt zu CHLINE, also alle Zeilen ihre Pixel und auch deren Zeichenzeit verlieren! Das Resultat war etwas womit selbst ein CRT Monitor nicht mehr auskommt, gibt schwarzes Bild. Demenstprechend ist diese Version unbenutzbar, nur als Dokumentation geeignet.

Programm "VGA Threaded" Version 6

Was bereits am selbigen Tag zur sechsten Version führte, welche wieder bis zu HPULSE identisch ist. Während der Suche nach der noch unbekannten Fehlerursache, habe ich die ganze Taktzyklenrechenrei überarbeitet, weil zuerst dort den Fehler vermutet. Dabei wurde HPULSE: wegen komplizierter Rechnerei zu einem einfachen Unterprogramm gemacht, mit der Fortsetzung in jedem der States drin. Dabei kamen statt den kollidierten ijmp, jetzt temporär ganz normale rjmp zum Zug, zwecks vereinfachen um den Fehler zu finden. Damit war keine Doppelbelegung vom ijmp mehr, kein zerdeppern. Das Resultat davon war immerhin wieder ein Bild, wenn auch ein total zerfleddertes.

Analyse ergab, dass es pro 2 Zeichenzeilen (und damit 16 Paare Videozeilen) jeweils die erste Videozeile der ersten Zeichenzeile und dann die anderen 7 der zweiten ausgab. Der Fehler lag in CHEND:, weil der Ende Zeichenzeile Test für nächste Zeichenzeile nach der zweiten Videozeile im Paar zeichnen nochmals ablief. Dies sauber auscodieren, wenn auch einiges länger, löste das Problem. Nur noch ein paar kleine Taktzyklenfehler mussten korrigiert werden, ein fehlendes nop in VRTBLINE:, sowie in CHLINE; falls leere Zeile, sowie alle VR*: ein weiteres nop. Danach wurde das Bild wieder korrekt.

Programm "VGA Threaded" Version 7

Eigentlich wäre es jetzt Zeit gewesen, die Statemachine auf die fertig vorbereitete Ansteuerung per Timer umzustellen, und damit erst Threading zu bekommen, und somit ein nicht mehr statisches Bild zu ermöglichen. Aber dann entstand eine IRC Diskussion mit Hans Franke vom VCFe, in dem ich ihm wegen erstmals seit 2 Monaten sehen das Projekt zeigte und dabei den Programmcode erklärte. Sein grösster Kritikpunkt war die Kommentare zu verbessern. Die Kommentare waren nach dem grösseren Umbau der letzten vier Versionen ohnehin wieder wegen aufräumen fällig. Was zur siebenten Version führte.

Dabei wurden im vga_text_font.fon alle p-A (pseudo-ASCII) Zeichen zu "block graphics" umbenannt, und alle für einen Fontdesigner bedeutungslosen aa=, hh=, ll=, cc= Werte eliminiert, mit statt dessen nur dem ASCII "Zeichen" und Zeichencode drin notiert.

Ebenso hat es keine aa=, hh=, ll=, cc= am Ende vom Programm bei den .include, weil besser erklärt, mit nur "ASCII * 128 Worte". Zudem wurde das Zeilenabbruch Pseudozeichen von DEL = 0x7F zu ABORTPSEUDO = 0x7F umbenamst. Ebenso gab es neu FIRSTPSEUDO = 0x09 und LASTPSEUDO = 0x0F sowie FIRSTCHAR = 0x10 und LASTCHAR = 0x7E, welche dann in FBWCHAR benutzt werden. Weiter wurde die Speicheraufteilung zwischen 9*128 Worten Programmcode und Rest Zeichen dort dokumentiert. Wozu auch die Spezialzeichen von 0x08..0x0E auf 0x09..0x0F verschoben wurden, um mehr Codeplatz nutzen zu können.

Im eigentlichen vga_threaded.asm geschah auch einiges. Am Anfang wurde die ganze Hardware weit besser dokumentiert, insbesondere der VGA Stecker, und die Berechnung und Belegung der VGA Hilfsplatine ihrer 3 DACs ihre beiden Widerstandswerte. Parallel dazu entstand auch das Photo der Schaltung Skitzen.

Beim Bildspeicher wurden die FBENDTOKEN = 0xBF und FBENDVALUE = 0xFF benamst und besser erklärt. Ebenso wurde die Umwandlung von ASCII Zeichencode in den Bildspeicher Zeichencode besser erklärt.

Bei der Zeichnerei wurden die 12,13,25 Konstanten ebenfalls benamst. Und statt C0 gibt es jetzt auch ein benamstes DRLINE Register. Neben VGA Text 400von450Zeilen/70Hz wurde ein zweiter Timing Satz für VGA Graphik 480von525Zeilen/60Hz addiert. Und alle States haben am Anfang einen Test, welchen davon sie in DRLINE laden, um die beiden VGA Modi umschalten zu können. Dieser Test muss wiederum bei Wiederaufruf kompensiert werden mit mehr nop vor dem rjmp am Ende. Ebenfalls wegen dies ist der Bildspeicher fest 30 Zeilen lang, mit 25 oder 30 davon genutzt. DEMO: wurde um eine 26ste Zeile call DEBLANK erweitert, die Zeilen 27..30 zeigen dann nur "leere" Zeilen mit alles Punkte, so wie FBINIT: dies vorbesetzt.

Bei den sichtbaren Zeilen wurden alle FORE und BACK und ALTn Register zu LUTFORE und LUTBACK und LUTALTn, was auch in der Segmentzeichenroutine sowie im diese erzeugenden genfont angepasst werden musste. Ebenso in den Spezialzeichen. Auch hier gibt es zwei Timing Sätze mit 25 bzw 30 Zeichenzeilen. Ebenso wird DRLINE benutzt statt C1, und zudem DRSEGM statt T1 und DRROW statt C0. Nach Leerzeilen hat es auch ein out VGADPORT,BLANK wie im Zeilenabbruch Pseudozeichen, für konsistentes Timing.

Gross verbessert wurde die Kommentierung der Segmentzeichenroutine selber, insbesondere beim "parallel" vom Zeichenalgorithmus. Dieses bereitete dem Hans Franke beim Lesen die meisten Probleme, und wurde daher komplett neu geschrieben. Auch dabei fielen die ganzen aa=, hh=, ll=, cc= Sachen komplett weg, weil sie nicht wirklich hilfreich waren. Ebenfalls wurde die Serie von 6 nop ersetzt durch eine einzelne .db "012345678901" Zeile welche 12 Zeichen und damit 6 Worte setzt, was kompakter ist und zudem sofort als nicht Programmcode erkennbar. Auch dies musste in genfont angepasst werden. Ebenso wurde dies in den Spezialzeichen und dem Pseudozeichen verwendet.

Wie in Version 1 bereits erwähnt, wurden bei den Registernamen alle C0..C3 Counter (Zähler) als Klasse beseitigt, und die A0..A3 Akkumulator wurden zu S0..S3 Saved. Dann wurde umgeordnet zu neu T0..T2 Temporary, S0..S2 Saved, I0..I1 Interrupt, alle mit Immediate, sowie einfach V0..V13 für alle Variablen ohne Immediate. Später wurden letztere zu F0..F13 Fast (Schnelle). AH:AL wurden zu DH:DL (Double). Die obigen DRLINE und DRSEGM und DRROW sind nun in S0..S2.

Neben besseren Kommentaren und Registerbelegung gab es auch etwas mehr Hardware Komfort. Statt für die Zeilenpaare einen Jumper an Port PD Pin PD6 gab es nun einen Satz von 8 DIP Switches an allen Portbits um mehr Optionen zu schalten. Daher wurden die MOD* zu LN2* für Doppelzeilen umbenamst, sowie auf Pin PD7 als SIZ* für Bildgrösse addiert.

Programm "VGA Threaded" Version 8

Jetzt wurde es Zeit, auf Betrieb mit dem Timer umzustellen, mit einer echten Interrupt Serviceroutine, und somit erstmals dem geplanten Multithreading auf das diese ganze Programmserie hinarbeitete. Was zur achten Version führte.

Als erstes wurde die Interrupt Vektortabelle am Anfang des Programmes ausgebaut. Dabei wurden alle Vektoren mit Namen versehen, um den richtigen davon zu treffen. Während VECRESET weiterhin sein jmp RESET hat, und alle unbenutzten ihr jmp UNUINT, bekommt VECT0CMP neu ein jmp T0CMPINT (Timer 0 CoMPare INTerrupt).

Bis zu WAIT: blieb alles andere wie gehabt. WAIT: selber wird nun als Teil der Interrupts benutzt, also wurde dessen Variable T0 durch I0 ersetzt. Ebenso werden DRLINE und DRROW und DRSEGM in S0..S2 zu neu VBLINE/CHLINE und CHSEGM und CHROW, in V4 bzw I1 bzw V5. Weil die V0..V13 keinen Immediate Mode kennen, müssen sie am Anfang von jedem State via I0 geladen werden. I1 kann aber direkt laden, und wird für CHSEGM benutzt wegen unterwegs mit Immediate Mode modifizieren. Auch alle CLUT Register in den V0..V13 werden um 6 nach oben verschoben. Wegen dem Registerverbrauch reduzieren wurde TEMP eliminiert, durch die dreimal mov TEMP=etwas, etwas=anderes, anderes=TEMP Logik ersetzet mit dreimal eor.

Neu müssen die vom Vordergrundthread benutzten ZH:ZL und XH:XL Register gesichert und restauriert werden, damit der Hintergrundthread nichts davon merkt. Wozu am Anfang von T0CMPINT: 4 push Befehle und am Ende von BACKGROUND: 4 pop Befehle dienen. Selbiges gilt auch für das AVR Statusregister SREG, welches via I0 bewegt wird. Alle anderen S* und T* Register bleiben unberührt, weil im Interrupt ja separate I* benutzt werden, dank den vielen Registern im AVR (aber mit nur wenigen davon als 16bit nutzbar). Aber auch der Vordergrundthread will seine Registerwerte ZH:ZL und XH:XL behalten, weshalb diese am Ende von BACKGROUND: in V0..V3 Register weggespeichert werden, und am (Wieder-)Anfang von T0CMPINT: aus diesen restauriert werden, vor dessen ijmp. Das alles ist standard Multithreading Verhalten, Registersaetze austauschen.

Was hier nicht gemacht wird ist jegliches Stack umstellen, da nur ein Hintergrundthread und ein Vordergrundthread. Ansonsten hat BACKGROUND: noch zwei Interrupt spezifische Sachen. Einerseits statt mit einem normalen ret endet eine Interrupt Serviceroutine mit reti, um die Interrupt Logik wieder freizuschalten für den nächsten Aufruf, weil diese während in einem Aufruf drin sein blockiert wird gegen Mehrfachaufruf Probleme.

Anderseits um zu verhindern, dass der nächste Aufruf sofort geschieht, muss noch der Wert 1<<OCF0 in das Register TIFR kopiert werden. Das liegt daran, dass der Timer jede Zeile einmal losgeht, aber die Zeichnerei dann HPULSE:+Zeile+HPULSE:+iret macht, womit der Timer bereits wieder vor dem zweitem HPULSE: losgegangen ist. Ohne Korrektur würde nach reti sofort wieder aufgerufen werden, statt erst auf den nächsten Timer losgehen für den dritten HPULSE: erzeugen.

Auch T0CMPINT: wartet mit etwas sehr speziellem auf. Eines der Grundregeln von Interrupts ist, dass der Prozessor sie nur einmal nach jedem einzelnen fertig ausgeführten Befehl abtesten kann, und falls losgegangen die Serviceroutine aufruft. Da manche Befehle aber bis zu 4 Takte brauchen, kann somit je nach gerade zufällig laufendem Befehl vom Hintergrundthread die Serviceroutine um bis zu 3 Zyklen verzögert werden, was als "Interrupt Latency" bekannt ist. Das ist bei den meisten Programmen kein Problem, weil die Reaktionszeit weit unter kritisch ist.

Aber hier sind das bis zu drei Takte und damit einem ganzen Pixel an Zeit zu spät kommen. Das erst noch mit HPULSE: und der ganzen restlichen Zeile davon betroffen, was die Zeile nach rechts verschieben wird, und den Monitor zeitlich verschieben mit der nächsten Zeile dann links. Und das kann bei jeder beliebigen Zeile zufällig passieren, sogar von Bild zu Bild verschieden. Was ein zeilenweise herumzuckendes Bild ergeben würde, was als "Jitter" Effekt bekannt ist. Dieses zuerst absichtlich noch nicht korrigiert sieht so zerjittert aus.

Um das zu verhindern sind in T0CMPINT die 3 mal 5oder4*nop+in+cpi+breq da, deren breq zu TSYNC1*: je um einen Takt mehr warten, falls nicht alle 3 Takte an Latency bereits verloren gingen. Womit sie also quasi auf die maximale Latency "auffüllen". Wenn man im unter Mikrosekunden Bereich auf Taktzyklen genau liegen will, muss man solche Sachen machen!

DISPLAY: ist nun etwas aufwendiger geworden, weil es die ganze Interruptlogik vorbereiten muss. Dabei dupliziert es teilweise das Verhalten von T0CMPINT:, nur mit zusätzlich cli als Interrupt Logik sperrer (ist bei echtem Interrupt automatisch), und ohne Jitter Korrektur. Dazu kommt noch die ldi+out+ldi+out+ldi+out Sequenz um den Timer auf die 75*8=600 Takte zu setzen. Ebenso muss es 1<<OCF0 in TIFR kopieren, um allfällige bereits losgegangene Timer zu ignorieren. Dann wird normal zu VRBBSTATE: gegangen.

VRBBSTATE: bleibt gleich, bis auf VBLINE: via I0 laden. VRBBLINE: benutzt normal call HPULSE, wird dann aber total anderst. Die ganzen 55+480 Zyklen abwarten sind weg, weil jetzt eben das Hintergundprogramm dran kommt. Dazu kommt hier der erste Aufruf von jmp BACKGROUND:, nach davor ZH:ZL zwecks ijmp mit VRBBCONT: (Vertical Retrace Bottom Blank CONTinue) laden. Dann wird der Hintergrundthread abgearbeitet bis der Timer losgeht, und der reaktivierte Vordergrundthread via T0CMPINT: und dessen ijmp zu VRBBCONT: springt. Auch das ist standard Multithreading Thread umschalten Verhalten. Falls VBLINE noch nicht 0 ist, wird nach den nop rjmp VRBBLINE gemacht, welches wieder call HPULSE benutzt, und dann ZH:ZL setzt, und erneut via BACKGROUND: abtritt. Wenn 0 wird mit breq VRPUSTATE weitergeschritten. VRPUSTATE: bleibt ebenfalls gleich, bis auf VBLINE: via I0 laden. Danach VRPULINE: plus VRPUCONT:, sowie VRTBSTATE: plus VRTBLINE: plus VRTBCONT: machen es auf die selbige Weise.

Dabei bekommt der Hintergrundthread pro leere Zeile 55+480 Takte, minus was BACKGROUND: und T0CMPINT: verbrauchen, was 19 bzw 48 sind, und so in 55+480-19-48=468 Takte resultiert. Wenn man nun noch die Zeile = 600 Takte nimmt, merkt man dass nur HPULSE: ausgeben 600-468=132 Takte kostet von denen der Puls selber 30 ist, und dass danach 468/600=78% der Zeit jeder Leerzeile vom Hintergrundthread nutzbar sind. Bei 200oder400 von 450 Zeilen gezeichnet, und somit 250oder50 leer, gibt das 250/450*78=43.33% oder 50/450*78=8.667% der AVR Leistung für den Hintergrundthread. Bei 240oder480 von 525 sind es 285/525*78=42.34% oder 45/525*78=6.686%. Wer jetzt bedenkt, dass der AVR gegenüber 1980 8bit je nach Prozessor und Anwendung 10 bis 100 mal schneller ist, bemerkt dass so immer noch gleich viel bis 3 mal mehr an Leistung verbleiben!

CHINIT: ist gleich. CHSTATE: und CHLINE: wurden wegen Namenskollision zu CHDRSTATE: und CHDRLINE: und CHDRCONT:. Falls es eine leere Zeile wird, gilt selbiges Timer abwarten wie oben. Bei gezeichneter wird das gemacht, bis zum nächsten HPULSE: durchgehend, genau identisch wie bisher. Die Kollision der zwei Nutzungen von ZH:ZL für ijmp in Version 5 wird auf einfache Weise gelöst, indem CHDRLINE: es bei jedem jmp BACKGROUND einfach neu setzt, egal was die Segmentzeichnerei davor damit angestellt hat! Also kommt nach CHDRSTATE: aufrufen und HPULSE: zuerst die Entscheidung ob leere Zeile, falls ja ZH:ZL=CHDRCONT dann call BACKGROUND. Wenn zurück von T0CMPINT: zu CHDRCONT:, erneut call HPULSE und entscheiden, falls zeichnen mit 41 mal ZH:ZL=Segment, und erneut call HPULSE und entscheiden, leer wieder ein ZH:ZL=CHDRCONT dann call BACKGROUND. Somit 100% robust.

Der ganze Demo Programmcode ist identisch. Nur RESET wird leicht anderst. Alle INIT Sachen sind identisch. Aber dann wird statt jmp DISPLAY nach call DEMO neu zuerst call DISPLAY aufgerufen, weil dieses ja nach der Interruptlogik vorbereiten als Vordergrundthread direkt in VRBBSTATE übergeht, welches bald seinen Aufruf von jmp BACKGROUND absetzt, wonach dessen reti den call DISPLAY "beendet", womit der Hintergrundthread nach dem call DISPLAY weitergeht. Jedes Timer losgehen wird via T0CMPINT: bis BACKGROUND: den Prozessor "ausleihen" und mit reti wieder "abgeben", und so Bild erzeugen. Neu muss jetzt erst das call DEMO kommen, aber in einer Endlosschleife damit der Prozessor mit der ganzen Hintergrundthread Zeit etwas zu tun hat. Alles normale standard Multithreading Techniken. DEMO: selber tut wegen identisch bleiben einfach immer wieder das selbige Bild malen. Und das alles lief gleich beim ersten mal, ohne auch nur einen nop korrigieren zu müssen!

Programm "VGA Threaded" Version 9

Wieder gibt es eine Runde lang aufräumen, nach einer derart grossen Änderung, bevor von den mit dem Multithreading geschaffenen Möglichkeiten gebrauch machen. Das resultierte in der neunten Version.

Als erstes gab es einen dritten Optionen Schalter an Pin PD5 als CO2*, um falls LN2* eine Leerzeile verlangt, diese wie bisher in schwarz oder neu auch in Hintergrundfarbe zu zeichnen. Dabei wird ebenfalls ohne Zeichnerei der Hintergrundthread betrieben. Dazu wurden CHINIT: und CHDRLINE: leicht erweitert. Dies entstand als weitere Folge obiger IRC Diskussion mit Hans Franke vom VCFe. Das Resultat gefällt mir aber gar nicht. Mit Zeilenpaar ist das Bild blockig, wie CGA Doublescan. Mit Zeichnen+Leer ist es streifig wie Homecomputer aber weil konsistent ist das einfach zu ignorieren. Mit Zeile+Hintergrundfarbe ist der Hintergrund nicht mehr streifig aber der Vordergrund schon, was wegen inkonsistent dessen Streifigkeit hervorhebt, so dass der Font selber gestreift wirkt!

In Vorbereitung auf verbessertes Demo wurde diverse Infrastruktur verbessert. FBINIT: wurden FBCOL/FBROW durch direkte S0/S1 ersetzt, und diese weil es S* sind mit push/pop gesichert. Ebenso XY*: and DEMO: ihre XYCOL/XYROW. FBWSTRING: bricht bei zu lange ab, statt auf nächste Zeile umzubrechen, und wird damit kürzer. Womit auch das FBFREND Byte und dessen Inhalt FBENDVALUE wegfallen, und am Ende von FBINIT: dieses setzen. Ebenso ist FBENDTOKEN weg, weil durch ABORTTOKEN ersetzt, welches erst bei ABORTPSEUDO definiert wird. Alles XY*: Zeugs wurde zu nach der ganzen Zeichnerei verschoben, weil es erst in DEMO: genutzt wird.

Das DEMO: wurde auch vorbereitend überarbeitet, vor allem modularisiert. Nach XY*: Zeugs wurde DELINE: addiert, welches nur XYWSTR und inc S0 macht. DEBLANK: schreibt nur noch von Spalte 0 aus 40 Leerzeichen, ohne sich um Farben zu kümmern. DEBLACK: addiert Farbe schwarz setzen. DEASC_3X32: gibt nur die ASCII Zeichen aus, DEASC_3X40: löscht das Feld darumherum und macht dann DEASC_3X32. DERAIN_1X40: setzt nur die Farben pro Zeile, ohne Muster, DERAIN_6X40: macht alle Zeilen, DEWAVE_1X23: eine Zeile Muster, DEWAVE_6X23: alle Zeilen. Auch DESEPAR_1X1: und DESEPAR_6X1: sind so zerlegt. DELOGO_6x16: nutzt einfach DELINE, ist sonst gleich. DEGRAPH_6X40: fasst dann alles vom oberen graphischen Block zusammen. DEANN_3X40: gibt den mittleren Textblock aus. DERGB_1X40: und DERGB_7X40: sind zerlegt die Farben setzen, DEMULT_1X40: und DEMULT_7X40: sind zerlegt die Zeichen ausgeben. DEWEB_2X40: gibt den unteren Textblock aus. DESCREEN: fasst alles zusammen. DEMO: ist nur das gefolgt von Endlosschleife mit nichts drin. RESET: endet in nur Sprung zu DEMO.

Programm "VGA Threaded" Final

Nach all diesem vorbereiten wurde es erst möglich, das Ziel dieser ganzen "Theaded" Programmserie zu erreichen, ein Bild welches während angezeigt werden auch vorzu verändert wird, statt nur einmalig aufgebaut und dann angezeigt. Womit man erst etwas animieren kann. Das resultierte in der zehnten und finalen Version.

Alles bis und mit FBWCOLOUR: bleibt gleich, danach wurde noch FBRCOLOUR: als Gegenstück addiert. Selbiges nach XYWCOLOUR: auch mit XYRCOLOUR:. Die ganzen BACKGROUND: T0CMOINT: und DISPLAY: wurden nach HPULSE: verschoben, womit DISPLAY: wieder direkt ohne rjmp in VRBBSTATE: hineinlaufen kann. Ansonsten läuft die ganze Ausgabe praktisch identisch ab.

Weil die Zeichnerei jetzt mit der Ausgabe synchronisierbar sein muss, wurde dazu noch CHROW in V5 welches bisher nur in CHINIT abwechselnd 0x00/0xFF (0/-1) hat erweitert, zu auch VBINSTATE sein, welches im Rücklauf mit VRINBB = -1 bzw VRINPU = -2 bzw VRINTB = -3 geladen wird. Dazu kam eine Hilfsroutine VBWAIT:, welche zuerst solange VBINSTATE = VRINBB wartet (falls gerade vor kurzem aus Bild raus), dann bis VBINSTATE = VRINBB ist, was genau den nächsten Übergang von Bild zu vollem Rücklauf erwischt, und somit maximal Zeit für Animation gibt. Dieses benutzt T0 und nicht etwa I0, weil es vom Hintergrundthread benutzt werden wird.

Alle anderen Änderungen sind in der Demo drin. Selbst alles bis und mit DESCREEN: bleibt gleich, weil dies nur das erste statische Bild aufbaut. Erst danach kommen vor RESET: die Animationen dazu. Ab RESET: ist wieder alles identisch. DEMO: selber ruft zuerst wie gehabt call DESCREEN auf, aber hat jetzt in der Endlosschleife zuerst rcall VBWAIT und dann die ganzen Animationen drin. Um diesen eine Zeitreferenz zu geben wird eine Variable S2 vor der Schleife auf 0 gestellt und darin immer +1 gerechnet. Damit werden alle Animationen in Bruchteilen von 256 Frames mit Nummern 0..255 ausgeben gesteuert.

Erste Animation ist Farben zyklisch verändern. Mit DEROTBG_1X40: wird unter Verwendung von call XYRCOLOUR+FBRCOLOUR sowie call XYWCOLOUR+FBWCOLOUR der Rotanteil der Hintergrundfarbe einer Textzeile in der Reihenfolge von 0..1..2..3..0.. verändert. Das subi T0,-0x10 reicht dazu, weil call FBWCOLOUR die oberen 2 Bits killt. Dann wird in DEROTBG_3X40: der Rest gemacht. Das andi T0,0x3F sorgt dafür dass nur jedes 64ste Frame die Farbe geändert wird, also in etwa 1s Schritten. Der Test mit PIND4 benutzt einen weiteren Optionen Schalter um diese Animation selektiv laufen zu lassen. Dies waren anfangs die 3 Textzeilen im Projektbeschrieb Teil vom Bild, also mit am Anfang noch ein ldi S0,11 statt später ldi S0,0.

Zweite Animation ist eine ASCII Art Schnecke _@_/ übers Bild bewegen. Hier wird in DESNAIL_1x40: zuerst eine allfällig bereits gezeichnete Schnecke mit dem ersten call DELINE mit Leerzeichen wieder gelöscht. Dann erst kommt der Test mit Optionen Schalter PIND3, ob es eine neue Schnecke geben soll, weil sie sonst nach abschalten an Ort unbewegt sichtbar bleiben würde. Mit dem andi T0,0x0F wird jedes 16te Frame bewegt, also etwa 1/4s Schritte. Dann wird die Position in der Zeile verschoben, und falls zuweit nach rechts wieder nach links gestellt. Dann wird die Schnecke neu gezeichnet. Dies war anfangs in der Leerzeile oberhalb der Zeilen mit dem Projektbeschrieb, also ldi S0,10 statt ldi S0,22.

Dritte Animation ist ein herumspringender Ball in einem Abschnitt der Wellenlinien Graphik. Dazu wird mit DEBOFIELD_1x6: und DEBOFIELD_6x6: ein Feld mit 6x6 Leerzeichen geschaffen. In dieses wird ein 3x3 Ball gezeichet, die restlichen bleiben Leerzeichen. Dieser erscheint ganz links als ASCII Art, passend zur Wellenlinie, ganz rechts aber als Blockgraphik, passend zum SoftVGA Logo, sowie dazwischen als 2/3 bzw 1/3 der beiden. Daher hat es auch DEBOBALL_AAA_3X3: und DEBOBALL_AAB_3X3: und DEBOBALL_ABB_3X3: sowie DEBOBALL_BBB_3X3: um die 4 Varianten zu zeichnen. Mit DEBOBALL_3X3: wird ausgehend vom Ort in T1 die passende davon benutzt. Dann wird in DEBOUNCE_6X40: der Rest gemacht. Auch hier wird zuerst mit DEWAVE_6X23: die Wellenlinie wiederhergestellt, um den Ball zu löschen, dann mit Optionen Schalter PIND2 getestet, dann mit DESEPAR_6X1: und DEBOFIELD_6x6: Platz geschaffen, dann Position gerechnet, dann mit DEBOBALL_3X3: dort gezeichnet. Position ist Zeilen und Spalten 0-2 1-3 2-4 oder 3-5, also muss je eine Koordinate 0..3 erzeugt werden.

Das Resultat dieser ersten drei Animationen wurde als erstes Video aufgezeichnet.

Danach gab es noch drei Korrekturen und eine Erweiterung. Als erstes war der Ball wegen ASCII Art nur ein Umfang aber Blockgraphik eine volle Fläche sehr variabel "schwer". Also wurde die Blockgraphik auf ebenfalls nur Umfang revidiert. Als zweites wurde die Schnecke von Zeile 10 oberhalb Projektbeschrieb auf Zeile 22 oberhalb Webadresse verschoben, für mehr Abstand von Ball und Farben. Als drittes wurde die Ballbewegung revidiert. Dazu wird neu Frame/8 vertikal bzw Frame/16 horizontal benutzt, beide zudem auf den Bereich 0..7 reduziert für 8 Positionen (4 hin und 4 zurück), sowie letzteres um 1 erhöht für 1/8 = 45° Phasenverschiebung. Sie entsteht die "liegende 8" Lissajou Figur als Bewegungsverlauf. (Was aber übersehen wurde, ist den Ball wegen Blockgraphik in Halbzeichen Schritten bewegen.)

Neu kam als vierte Animation ein blinkender Cursor in der ersten Zeile vom Projektbeschrieb. Hier wird in DECURSOR_3X40: wieder zuerst der Projektbeschrieb mit DEANN_3X40: wiederhergestellt, dann mit Optionen Schalter PIND1 getestet, dann jedes zweite Frame kein Cursor gezeichnet damit abwechselnd Zeichen oder Cursor dort steht, mit Frame/16 und Bereich 0..1 entscheidend ob überhaupt Cursor. Dann verbleiben von der Frame Nummer noch 3 Bits, den welche Cursor auf Zeichen 0..7 positionieren. Wegen dies im Projektbeschrieb geschehen wurde die Farbzyklerei in die ASCII Zeichensatz Ausgabe zuoberst verschoben.

Das Resultat dieser insgesammt vier Animationen wurde als zweites Video auch aufgezeichnet. Das ist die letzte Evolutionsstufe dieser Programmserie.

Algorithmus "Indirect"

Die grösste Folge obiger IRC Diskussion mit Hans Franke vom VCFe kommt aber erst hier. Ihm gefiel der massive Verbrauch von (128-9)/128 = 93%, von 32k = 29.75k, von Programmflash nicht, selbst wenn grossteils mit genfont erzeugt. Strikte wird der AVR Typ ohnehin durch die notwendige Menge von 2k SRAM bestimmt, und damit sind auch 32k Flash schlicht gegeben, also ist mit weniger davon füllen keine Ersparnis zu holen. Aber unschön ist es trotzdem. Und wie ich gegen Schluss immer mehr bemerkte, fing mit nur noch 2.25k für Programmcode der Platz für Ausgabe plus Demo an knapp zu werden, was auch bei Ausgabe plus Terminal beinhalten gelten wird.

Sein erster Vorschlag, die Segmente nicht als 8*(10+6) sondern als 8*6+48 Worte anzuordnen, und Demo (bzw Terminal) Code in die resultierenden 128 48er (welche immerhin 6k ergeben) zu verteilen gefiel mir aber aus logistischen Gründen gar nicht.

Tags darauf kam er mit einer total anderen Idee. Die Segmentsorten Routinen werden dabei nur als Satz von alle 16 genau einmal abgespeichert. Das nächste Segment bestimmen wird dabei aber aufwendiger, mit nicht mehr nur Zeichencode direkt zu Segment, sondern indirekt via einer Tabelle zum Segment. Diese Segmente passen so aber wenigstens in nur 1*256 Worte, und sind damit immerhin mit 8bit Arithmetik auswählbar. Dabei ist ZH konstant die Page der 256 Worte (weil ijmp) der Segmentsorten Routinen. YH ist ebenso die Page der 256 Bytes (weil lpm) Segmentidentifikator der jeweiligen Videozeile aller 256 Zeichen. Für die 8 Videozeilen/Zeichenzeile hat es dann 8 solcher Tabellen welche mit YH angewählt werden. Segment bestimmen wäre so Zeichencode ld ,X+ zu YL, Segmentidentifikator lpm ,Y zu ZL, Segment ijmp.

Aber grosses Problem bei diesem Ansatz ist, dass dazu eine Tabelle der Segmente benötigt wird, welche im Flash liegen muss. Diese ist zwar nur noch maximal 8Segmentindizes*256Zeichen = 2kBytes, trotz für 256 statt 112 Zeichen Platz bieten, muss aber wegen in Flash liegen mit einem lpm Befehl ausgelesen werden. Dieser bringt gleich zwei Probleme mit sich:

Einerseits braucht der lpm 3 Takte, was mit aller 4*(1+2)=12 Takte Zeichenalgorithmen Logik unverträglich ist, und somit auch mit 160 Pixel und 40 Zeichen zu je 4 davon! Hans sein Vorschlag die Pixel nicht strikte auf 4*3 Takte zu legen, sondern mit nur bei den "ändernden" out Befehle setzen, und die restliche Zeit flexibel ausnutzen, mit wo lpm kollidiert mehreren "optimierten" Segmentvarianten teilweise um einen Takt zu verschieben gefiel mir genauso aus logistischen Gründen gar nicht. Zudem würde die Menge an "optimierten" Segmenten es allenfalls auf mehr als die 1*256 Worte vergrössern. Wobei allerding nur genau die beiden abwechselnden FORE/BACK/FORE/BACK und BACK/FORE/BACK/FORE Segmente wirklich problematisch sind, weil sie die vollen 4 out brauchen.

Die Grundidee gefiel mir aber, weshalb ich sie weiter analysierte. Dabei stellte ich fest, dass die FORE/BACK/FORE/BACK und BACK/FORE/BACK/FORE Kombination immerhin einige Zeichen betrafen, alle " # % & 0 4 6 8 9 sowie @ A B D G H K M N O P Q R U V W X Y ^ und a b d e g h k n o p q r u v w x y. Was auf insgesammt 1/4 der Ziffern and Satzzeichen, sowie 1/2 der Buchstaben, aber gar keine der Blockgraphik hinauslief. Was ich noch akzeptierte.

Anderseits hat der lpm eine weitere saftige Designlimite, dass auf dem AVR neben dem ijmp auch der lpm nur mit ZH:ZL geht, was soviel Register umkopieren brauchte, dass es ohnehin nicht mehr in 12 Zyklen passen konnte! Statt dessen ging ich um Zeit zu bekommen auf eine 4*(1+3)=16 Takte Zeichenalgorithmen Logik, was allerdings nur 120 statt 160 Pixel und somit nur 30 statt 40 Zeichen erlaubte. Anderseits war 3 Takte lpm dann auch kein Problem mehr, ohne jegliche um einen Takt daneben liegende Pixelgrenzen. Das Resultat sieht so aus:

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 1 ausgeben
                     ;        aus 4* LUTFORE oder LUTBACK entstehen Pixelmuster
ld ZL,Y+             ; {2}  nächstes Zeichen: Zeichennummer holen
                     ;        ist untere 8bit Fontadresse (lpm Byteadresse)
mov ZH,CHSEGM        ; {1}    und wähle passende Font Segmentindex Tabelle
                     ;        ist obere 8bit Fontadresse (lpm Byteadresse)

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 2 ausgeben
lpm I0,Z             ; {3}  nächstes Zeichen: lese Segmentindex
                     ;        ist untere 8bit Segmentadresse (ijmp Wortadresse)
                     ;        lpm kann nicht direkt nach ZH or ZL laden!

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 3 ausgeben
mov ZL,I0            ; {1}  nächstes Zeichen: kompensiere lpm Limite
ldi ZH,high(SEGM)    ; {1}    erweitere diese mit Segmentroutinen Page
                     ;        ist obere 8bit Segmentadresse (ijmp Wortadresse)
nop                  ; {1}  unbenutzt: rest von Pixel abwarten

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 4 ausgeben
nop                  ; {1}  unbenutzt: rest von Pixel abwarten
ijmp                 ; {2}  nächstes Zeichen: weiter zu dieses

                     ;      keine nop mehr, kein auffüllen auf 16 Worte
                     ;        vielmehr hat es bewusst unter 16 Worte/Segment
                     ;        weil Spezialsegmente und Pseudosegment in 256 W
    

Wie man sieht, hat es sogar 2 nop abwarten darin. Diese sind aber nur 2 der 4 Mehrtakte, welche durch die Umstellung auf 4*(1+3) Takte entstehen. Was auf 2 Takte zuwenig bei 4*(1+2) hinweist. Wieder hat es die 4 mal out VGADPORT,LUT???? drin. SEGM???? definiert 16 Segmente 0000 bis 1111, mit der 2^4=16 Pixelmustern. Die Kommentare zeigen genau was zu aktuelles Zeichen ausgeben und was zu nächstes Zeichen sein Segment "parallel" bestimmen gehört. Man sieht hier auch, wie sehr die Kommentierung gegenüber dem Code vom "Direct" Algorithmus verbessert worden ist.

Die YH:YL haben Adresse der Zeichennummer im SRAM drin. CHSEGM hat nicht nur Anfang der ganzen Tabelle drin, sondern auch Videozeile in Zeichenzeile dazu, weil es 8 Tabellen zu je 256 Zeichen sind, jede in einer Page, mit pro Tabelle nur ein Segmentindex pro Zeichen drin, weil das so schneller geht. CHSEGM ersetzt so auch das ori ZL,0x?0 mit ? = 0..7 in der "Direct" Methode. lpm holt den Segmentindex, via I0 zu ZL, weil direkt zu ZL im AVR auch nicht geht. Ohne diese weitere Limite wäre dort ein drittes nop. Auch die 16 Segmentroutinen passen in eine 256 Worte Page, deren Adresse in Page genau der Segmentindex ist, weshalb nur noch ldi die Pagenummer setzen muss. Da dies eine Konstante ist, muss sie nur geladen werden weil lpm und ijmp beide nur mit ZH:ZL laufen. Sonst wären mov ZH,CHSEGM und ldi ZH,high(SEGM) nur einmal vor der Zeile nötig. Damit wären es 5 nop, und man könnte mit 4*(2+1) Takte und den "optimierten" Segmenten arbeiten!

Aber es gab, wie mir bereits am gleichen Tag wie obiges Zeichen auf die Kombination analysieren auffiel, eine weit bessere Idee, den 3-Takte lpm eliminieren durch die Tabelle einfach in SRAM ablegen mit dort einen 2-Takte ld dafür benutzen. Damit kollidieren ld und ijmp ebenfalls nicht mehr in der Benutzung von ZH:ZL, brauchen kein Register umkopieren. Als Nebeneffekt davon ist erst noch der Font in SRAM definiert und damit wie Bildspeicher zur Laufzeit bearbeitbar, was auf ladbare/modifizierbare Softfonts hinausläuft, und damit Zellgraphik statt bloss Textgraphik!

Problem dabei ist aber der SRAM Verbrauch für den Font, was beim ATmega32 mit 2k SRAM auf Font von 128 Zeichen in 1k und Rest auch nur noch 1k hinauslief. Die 128 Zeichen sind immer noch mehr als die bisherigen 112, akzeptabel. Dazu kommt aber noch, dass das SRAM der ATmega32 bei 0x0060 anfängt, was 32 Bytes vor dem 8*128 Font auslassen braucht, mit nur 992 danach. Aber nur 32+992 Rest erlaubt kein 40x30 mehr, und ist wegen Farbbytes selbst mit 40x25 überfordert, real liegen so Spalten -8 auf 32x25 oder Zeilen -4 auf 40x21 oder beide -4 und -2 auf 36x23 drin, wovon ich auf letztes hin entschied. Eine Reduktion auf nur 96 Zeichen, in 3/4k mit Rest 5/4k, habe ich wegen Timing verworfen. Das Resultat sieht so aus:

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 1 ausgeben
                     ;        aus 4* LUTFORE oder LUTBACK entstehen Pixelmuster
ld XL,Y+             ; {2}  nächstes Zeichen: Zeichennummer holen
                     ;        ist untere 8bit Fontadresse (SRAM adresse)
                     ;        XH ist fest obere 8bit Fontadresse (SRAM adresse)

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 2 ausgeben
or XL,I0             ; {1}  nächstes Zeichen: untere 8bit Fontadresse
                     ;        korrigieren, da Font ja 4*2*128 gross ist
                     ;        I0 ist fest untere 8bit Fontadresse, 0 oder 128
nop                  ; {1}  unbenutzt: rest von Pixel abwarten

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 3 ausgeben
ld ZL,X              ; {2}  nächstes Zeichen: lese Segmentindex
                     ;        ist untere 8bit Segmentadresse (ijmp Wortadresse)
                     ;        ZH fest high(SEGM*) Segmentroutinen Page
                     ;        ist obere 8bit Segmentadresse (ijmp Wortadresse)

out VGADPORT,LUT???? ; {1}  aktuelles Zeichen: Pixel 4 ausgeben
ijmp                 ; {2}  nächstes Zeichen: weiter zu dieses

                     ;      keine nop mehr, kein auffüllen auf 16 Worte
                     ;        vielmehr hat es bewusst unter 16 Worte/Segment
                     ;        weil Spezialsegmente und Pseudosegment in 256 W
    

Wie man sieht, hat es sogar 1 nop abwarten darin, trotz nur noch 4*(2+1) Takte. SEGM???? definiert wieder 16 Segmente 0000 bis 1111, mit der 2^4=16 Pixelmustern. Die Kommentare sind wie gehabt. Die YH:YL haben Adresse der Zeichennummer im SRAM drin. I0 hat Bit7 von der Videozeile in Zeichenzeile drin, weil es 8 Tabellen zu je 128 Zeichen sind, je 2 davon in einer Page. I0 Bit7 zusammen mit XH Bit1und0 ersetzt so auch das ori ZL,0x?0 mit ? = 0..7 in der "Direct" Methode. Das zweite ld holt den Segmentindex, direkt zu ZL. Dieses wirkt mit konstantem ZH, was die Nachladerei nicht mehr braucht und so erst 12 Takte und 160 pixel und 40 Zeichen erlaubt. Wenn diese bloss in das SRAM passen würden!

Alle dies kann mit einer ATmega644 mit 4k SRAM als 2k+2k gelöst werden, für volle 256 Zeichen und 40x30 mitsammt Farbbytes. Ich hatte aber zu dem Zeitpunkt keinen ATmega644, daher habe ich zuerst versucht für den ATmega32 obiges mit dem lpm zu machen, weil lieber die 40x25oder30 haben als die Softfonts. Ich bin erst dabei auf die ijmp mit lpm Kollision und folglich doch nur noch 30x25oder30 gestossen. Was dann die 36x23 (oder selbst 32x25) plus Softfonts weit attraktiver machte. Aber trotzdem habe ich die bereits angefangene lpm Version fertig gemacht, und erst danach auf ld umgestellt.

Programm "VGA Indirect"

Aufbauend auf diesen Algorithmus gab es 11 Programmversionen:

Programm "VGA Indirect" Version 1

Auch diese Version hat noch gar kein "Indirect" drin, nur diverse Programmcode Verbesserungen und auch leichte Fehlerkorrekturen, aber auch Vorbereitungen auf den grossen Umbau, was in der ersten Version hier resultierte. Selbst das Programm ist immer noch in einer Datei vga_threaded.asm, statt vga_indirect.asm wie dann ab Version 2, der Font dazu ist weiterhin in vga_text_font.inc. (Es hätte ebenfalls im Nachhinein besser als Programm "VGA Threaded" Final abgespeichert werden sollen, mit obigem Final als Version 10.)

Als erstes wurde die ganze Reihenfolge im Code systematischer. Zuerst alles zur Hardware beschreiben und definieren (aber die PIND* sind nur 7..5 hier definiert, mit 4..1 erst in der Demo vor Ort). Dann alle Konstanten Namen und Werte definieren, sowie Register allozieren und Namen definieren. Hier wurden auch die R2..R15 von V0..V13 zu F0..F13 umbenannt, und die explizit benamsten Variablen in der Richtung F15..F2 alloziert. Dann alle SRAM Variablen Namen und Platz allozieren und deklarieren (hier mit auch denen für die Demo drin). Dann aller Code, anfangend mit Vektoren und unbenutzen Interrupt, DBGLED*:, STR*:, FB*: (aber kein XY*:), alles zeichnende (aber mit VBWAIT:), XY*:, DE*., RESET:. Dann kommt aller leerer Platz. Zuletzt kommen alle mit ijmp angesprungenen Sachen, ab .org $0480, weil (128-9)*128 Bytes, mit zuletzt ab .org $3F80 das Zeilenabbruch Pseudozeichen.

Dabei auch konsistent umbenammst, CH[TG]ROWS zu CHDR[TG]ROWS wie schon bei VB*[TG]ROWS. Dabei auch CHLINE: direkt mit eor löschen statt mit ldi+mov. Vor CHDRLINE: auch CHDRROW: und CHDRSEGM:, damit jede Schleife ihren eigenen Anfang hat, was später ausgenutzt wird.

Vorbereitend auf die komplexeren "Indirect" Zeichenalgorithmen in T0CMPINT: und DISPLAY: sowie BACKGROUND: auch push/pop von YH:YL. Alle XH:XL YH:YL ZH:ZL kommen bei push H vor L und pop L vor H. Neu gilt zudem Policy, dass YH:YL bleibendes und XH:XL temporäres drin haben wird, analog zu S* und T* 8bit Variablen. Daher wird auch Frame lesen mit YH:YL statt XH:XL sein, was aber zu einem Absturz führte. Mit in Leerzeilen kein zurück zu Hintergrundthread gab es wenigstens ein Bild, wenn auch komplett verbeult und unvollständig. Als Ursache erwies sich eine simple Auslassung, nur in genfont das ,X+ zu Y,+ gewechselt, und somit auch in vga_text_font.inc, aber nicht in vga_text_specials.inc gemacht.

Hier sieht man unter anderem, wie es schräg aufwärts laufende hellere Linien hat. Diese sind was passiert wenn bei vertikalem Rücklauf nicht schwarz gestellt wird. Auch die Verläufe der Helligkeit ausserhalb vom Bild kommen davon, plus dass diese ihre Position ein Hinweis auf Zeilenzeiten nicht mehr konstant sein sind. Das gilt gerade auch oben rechts, wo die zusätzliche Helligkeit ein "S" Muster am rechten Rand hat. Auch hier ist sowas nur mit volldummen CRT sichtbar, weil halbintelligentes LCD längstens ausgestiegen wäre.

Programm "VGA Indirect" Version 2

Damit war alles bereit um auf "Indirect" in der lpm Variante umzustellen, was in der zweiten Version hier resultierte. Das Programm ist jetzt in der Datei vga_indirect.asm. Diese erhielt einiges an Änderungen.

Wegen dem kompakten Font und somit 256 Zeichen gibt es eine neue Aufteilung. Die FIRSTUNUSED = 0x00 und LASTUNUSED = 0x08 sind weg. Die Namen der Spezialzeichen ALT3,ALT2,ALT1,ULON,ULOFF,INON,INOFF sind neu hier definiert, statt in vga_text_specials.inc welches nicht mehr exisitert. Neu gibt es auch FIRSTGRAPH = 0x80 und LASTGRAPH = 0xFF weil mehr Platz.

Wegen den 4 Takten/Pixel vom neuen Algorithmus hat es auch weniger Zeichen, und dazu angepasste FBCOLS = 30 statt = 40. FBWCHAR: muss nicht mehr auf kleiner als niedrigstes testen, da dies jetzt 0x00 ist, noch auf grösser oder gleich höchstes, da dies jetzt 0xFF ist. Nur noch auf genau gleich ABORTPSEUDO muss getestet werden. Auch entfällt vom ASCII auf Zeichencodes umbauen, da der Bildspeicher jetzt direktes ASCII bekommt. FBINIT: muss weiterhin das Zeilenabbruch Pseudozeichen selber setzen, aber direkt als ABORTPSEUDO statt als ABORTTOKEN.

Alles bis und mit CHINIT: bleibt gleich, inklusive alles im vertikalen Rücklauf. Erst CHDRSTATE: wird mit dem neuem Algorithmus anderst. CHDRROW: kommt vor dem ldi CHSEGM, CHDRSEGM: vor dem eor CHLINE, nur CHDRLINE: bleibt vor dem rcall HPULSE. Dies weil die Steuerung der Videozeilen in den Zeichenzeilen total anderst ist. Die Leerzeilen bleiben aber gleich. Erst ab CHFIRST: bzw eigentlich CHDOUBLE: wird es wirklich anderst. Das erste Zeichen vorbereiten muss jetzt mit dem "parallel" vom neuen Algorithmus ablaufen. Die einmalige kommentierte Routine ist aber an den Schluss verschoben, siehe weiter unten, ebenso die Zeilenabbruch Pseudozeichen Routine. CHEND: muss wie anfangs CHDRSTATE: sich an die Steuerung der Videozeilen anpassen. Jetzt ist nur ein inc CHSEGM statt einem subi CHSEGM,-0x10, weil es um genau eine Page der 8 Pages zu je 256 Bytes im lpm Font verschieben geht statt um 16 Worte im ijmp Font.

Erstaunlich ist die Demo trotzdem fast komplett gleich gebleiben, mit nur 30 statt 40 Zeichen. Dies weil alle XY* auf zu lange Zeilen testen und einfach nichts zeichnen. Nur die zweite Animation mit der Schnecke lief nicht und musste nachträglich korrigiert werden. Der Fehler war in STRLDI, mit schon seit Anfang ld MH,X+ und ld ML,X+ vertauscht, jetzt ohnehin auf YH:YL umgestellt, mit ld YL,X+ und ld YH,X+. Sowie ebenfalls seit Anfang kein Test auf Buffer überlaufend, jetzt addiert. Aber als Seiteneffekt der Tests wurde hier die Zeile löschen statt spezifisch mit 4 Leerzeichen auf mit DEBLANK: umgestellt. Auch wurden alle STRLDI: mit "... " (Leerzeichen am Ende) ersetzt durch aussagekräftigeres "...", 0 (Nullbyte am Ende, kein Zeichen).

Danach kommt das Ende, welches wieder ganz anderst ist. Der Anfang davon ist nicht mehr mit .org $0480 um 59.4 der 64 Wortpages für 128-9=119 Zeichen zu je 128 Worte zu belegen. Sondern mit .org 0x3B00 um 256 Worte (1 Page) mit SEGM*: Zeichenroutinen und 2048 Bytes (8 Bytepages) (= 1024 Worte (4 Wortpages)) mit Font zu belegen. Hier kommt dann die einmalige kommentierte Routine SEGM: vom neuen Algorithmus, direkt gefolgt von alle 16 Varianten der Zeichensegmente SEGM0000: bis SEGM1111: jede nur einmal. Diese haben unter anderem ein mov ZH,CHSEGM wegen 1 Page im lpm Font statt or ZL,CHSEGM wegen 16 Worte im ijmp Font. Diese haben auch kein .db "012345678901" mehr um 6 von 16 Worte aufzufüllen. Danach kommt ebenfalls nur einmal das Pseudosegment für das Zeilenabbruch Pseudozeichen SEGMABRT:, nur out VGADPORT,BLANK und jmp CHEND, wieder ohne .db "01234567890123456789012345". Dann kommen die Spezialsegmente für die Spezialzeichen, SEGM0FXB: für INON und ULON, SEGM1BXF: für INOFF und ULOFF, SEGM0FX1: für ALT1, SEGM0FX2: für ALT2, und SEGM0FX3: für ALT3. Das alles muss in eine 256 Worte Page passen.

Danach folgt der Font. Mit .org 0x3C00 noch 4 Wortpages 0x3C bis 0x3F, passend für die 8 Bytepages von 8*256=2048 Bytes. Dort ist nur FONT: und dann .include "vga_text_font.inc". Es hat kein .include "vga_text_specials.inc" mehr, weil deren Spezialsegmente schon hier sind, und die eigentliche Definition ebenfalls in vga_text_font.inc drin ist. vga_text_font.fon sieht sehr ähnlich aus, hat aber jetzt Platz für 256 Zeichen. 0x00 bis 0x08 wurden als Leerzeichen addiert. 0x09 bis 0x1F wurden als Spezialzeichen addiert. 0x10 bis 0x1F Blockgraphik sind identisch, ebenso 0x20 bis 0x7E. 0x7F wurde als Zeilenabbruch Pseudozeichen addiert. 0x80 bis 0x8F hat Strichgraphik addiert. 0x90 bis 0xFF sind unbenutzt.

Gross anderst wurde aber genfont. Schliesslich muss es nicht mehr wie bisher die ganzen Segmente ihren Programmcode generieren, welche nun einmalig im Programm eingebaut sind. Neu muss es nur noch die passenden Segmentadressen auflisten, pro Videozeile eines Zeichen nur 1 Byte (statt 16 Worte), welches die unteren 8bits der Adresse deren Segmentroutine in der Segmente Page angibt. Daher haben die jetzt alle SEGM* Namen bekommen, weil genfont nur solche Zeilen generiert. Anderseits müssen diese neu geordnet werden, weil zuerst Tabelle für Videozeile 0 aller 256 Zeichen kommt, dann für 1 aller 256, dann 2 aller 256, bis 7 aller 256, in je eine der 8 Bytepages 0x78..0x7F (in den 4 Wortpages 0x3C bis 0x3F). Die ersten 4 ASCII Ziffern 0x30 bis 0x33 vom Font sehen als .fon bzw .inc Dateien dann so aus:

aus in vga_text_font.fon:

CHAR ASCII "0" 0x30
........
..()....
()..()..
()..()..
()..()..
()..()..
..()....
........

CHAR ASCII "1" 0x31
........
..()....
()()....
..()....
..()....
..()....
()()()..
........

CHAR ASCII "2" 0x32
........
()()....
....()..
....()..
..()....
()......
()()()..
........

CHAR ASCII "3" 0x33
........
()()....
....()..
..()....
....()..
....()..
()()....
........


wird in vga_text_font.inc:

; segment 0
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM0000), low(SEGM0000),  low(SEGM0000), low(SEGM0000)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen

; segment 1
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM0100), low(SEGM0100),  low(SEGM1100), low(SEGM1100)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen

; segment 2
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM1010), low(SEGM1100),  low(SEGM0010), low(SEGM0010)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen

; segment 3
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM1010), low(SEGM0100),  low(SEGM0010), low(SEGM0100)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen

; segment 4
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM1010), low(SEGM0100),  low(SEGM0100), low(SEGM0010)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen

; segment 5
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM1010), low(SEGM0100),  low(SEGM1000), low(SEGM0010)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen

; segment 6
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM0100), low(SEGM1110),  low(SEGM1110), low(SEGM1100)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen

; segment 7
  ... davor  Zeichen 0x00..0x2F, in  48/4=12 Zeilen
  .db low(SEGM0000), low(SEGM0000),  low(SEGM0000), low(SEGM0000)
  ... danach Zeichen 0x34..0xFF, in 204/4=51 Zeilen
    

Reicht das für die normalen Zeichen, muss genfont jetzt aber auch noch das Pseudozeichen und die Spezialzeichen erzeugen. Dazu kann es neben dem ........ bis ()()()() Mustersyntax auch weiteres erkennen. Ein -------- erzeugt ein low(SEGMABRT). Die ..FF<>BB und ()BB<>FF erzeugen dann low(SEGM0FXB) bzw low(SEGM1BXF) für revers, je nach ob "ein" alles BACK oder "aus" alles FORE. Die ..FF<>A1 bis ..FF<>A3 erzeugen dann low(SEGM0FX1) bis low(SEGM0FX3) für alternative Farben. Deren Gebrauch kann man im neuen Font bei Zeichen 0x09 bis 0x0F sowie 0x7F nachschauen. Das Makefile wurde auch an dies generieren angepasst.

Der erste Ablauf ergab bereits ein stabiles Bild, mit den richtigen Elementen der Demo auf den richtigen Zeilen, aber dessen Inhalt war etwas verstreut, teils verschoben, teils schlicht die falschen Zeichen. Schuld war ein Fehler in genfont, nur das korrigiert ergab bereits etwas ordentliches, mit wie erwartet lediglich die ersten 30 von 40 Zeichen der Demo.

Programm "VGA Indirect" Version 3

Damit lief der neue Algorithmus erstmals, aber es konnte noch einiges verbessert und aufgeräumt werden, was zur dritten Version führte.

Die eigentlich gar nicht benutzten Konstanten FIRSTPSEUDO und LASTPSEUDO sowie FIRSTBLOCK und LASTBLOCK sowie FIRSTASCII und LASTASCII sowie FIRSTGRAPH und LASTGRAPH flogen wegen unbenutzt heraus. Nur das ABORTPSEUDO blieb noch, weil als einziges davon benutzt. Anderseits gab es an Variablen nach der Schnecke ihr DESNPOS auch für den blinkenden Cursor ein DECUPOS.

Die Zeit um Flash zu laden ging mir auf die Nerven, also wurde das ohnehin viel kleinere Segmente+Font Zeugs jetzt von hinten an 0x3B00 bzw 0x3C00 nach vorne nach 0x0100 bzw 0x0200 geholt. Nur die Vektoren und die DBGLED*: Sachen sind noch vor diesen. SEGM*: bleibt identisch. FONT: hat den normalen .include, aber danach hat es noch um sicher zu gehen ein .org 0x0600, für den restlichen Code ab dort.

Alles STR*: Zeugs wird erst in DEMO: aufgerufen, und wurde jetzt dorthin verschoben, nach der ganzen Ausgabe. Also folgt jetzt gleich FBWCOLOUR:. Neben FBWCOLOUR: und FBRCOLOUR: bekam FBWCHAR: auch sein FBRCHAR: Gegenstück. FBWSTR: wird nur in DEMO: benutzt, und benutzt selber FBWCHAR:, wurde daher als STRWRITE: zu den STR*: Sachen verschoben. WAIT: ist generisch, wurde daher vor BACKGROUND: und T0CMPINT: verschoben. Diese wurden umgetauscht, weil in einem Interrupt zuerst T0CMPINT:, dann Zeilen, dann BACKGROUND: dran kommen. DISPLAY: kommt nach BACKGROUND:, und hat wieder sein rjmp VRBBSTATE, weil nicht mehr direkt vor VRBBSTATE:. Weiter wurden diese beiden besser Kommentiert, wie der ganze Interrupt Ablauf vor sich geht, sowie wie die Jitter Korrektur wirkt, auf Anfrage von Hans Franke. Erst danach kommen SYNCINIT: und HPULSE:, gefolgt von den Zeilen zeichnen.

Nach der ganzen Zeichnerei kommen nun die verschobenen STR*:, inklusive STRWRITE:. Dann die XY*:, inklusive Platz für den fehlenden XYRCHAR:. XYWSTR: benutzt nun STRWRITE:. DELINE: ist neu XYWLINE:, weil auch Teil der Infrastruktur. In der eigentlichen Demo wurde nur die vierte Animation mit dem blinkenden Cursor überarbeitet. Diese benutzt DECUPOS um wie die Schnecke mit ihrem DESNPOS über die ganze Bildbreite zu laufen, statt nur in den ersten 8 Zeichen. DEMO: selber setzt jetzt die beiden auf 0.

Programm "VGA Indirect" Version 4

Damit war zwar alles intern sauber. Aber die Demo war immer noch auf den alten Algorithmus seine 40x25 Zeichen und nur 0x20..0x7E Font ausgelegt. Zeit zumindest letzteres zu verbessern, was zur vierten Version führte.

Dazu wollte ich mehr Graphikzeichen, also wurde der Font ausgebaut. Er wurde dabei immer unübersichtlicher, also wurde genfont erweitert um Kommentare in der Fontdatei zu erlauben. Damit wurde auch die ganze Zeichensatz Aufteilung dort beschrieben, und einzelne Abschnitte markiert. Die ganzen Spezialzeichen ihre Namen im Program zu definieren gefiel mir auch nicht, erst recht im Hinblick auf immer mehr nicht-ASCII Zeichen im Font, also wurde genfont erweitert um Namen zu definieren. ABORTPSEUDO wurde aus dem Programm entfernt und dabei zu ABORT. ALT1 bis ALT3 ebenfalls und wurden dabei zu COL1 bis COL3, mit ebenso im Programm die LUTALT1..3 zu LUTCOL1..3. Ebenfalls im Programm wurden VBINSTATE zu VRINSTATE, sowie VBLINE zu VRLINE, und VBWAIT zu VRWAIT.

Die Blockgraphik wurde von 0x1? zu 0x8? verschoben und benamst als BGxxxx, mit xxxx als 0000 bis 1111 wie bei den SEGMxxxx. Wobei die Blöcke jetzt codiert werden als unten/rechts Bit0, unten/links Bit1, oben/rechts Bit2, und oben/links Bit3. In 0x9? gab es Balkengraphik BRxxxx. Die Strichgraphik wurde von 0x8? zu 0xA? verschoben und benamst als FSxxxx. Dazu kamen runde Ecken in 0xB? als FRxxxx. Sowie diverse Linien und Ecken in 0xC? als HL0..3 VL0..3 CRxx und TRxx.

Danach gab es nur noch die überarbeitete Demo. Vor allem wurde die dritte Animation mit dem herumspringenden Ball verbessert. Der Ball besteht nun aus 3x3 FRxxxx Zeichen, statt ASCII Art und Blockgraphik, und der Rest vom 6x6 sind FS0000. Dazu kommt noch ein Rahmen aus FSxxxx Zeichen. Dafür fehlt die ASCII und Block in 4 Stufen gemischt Logik. Auch die vierte Animation mit dem blinkenden Cursor wurde leicht besser, statt einem Unterstrich Zeichen (ASCII 0x5F) wurde ein Graphikzeichen benutzt. Versucht wurde zuerst HL3 (unterste 2 von 8 Pixelzeilen), aber BG1111 (alle Pixelzeilen) erwies sich als besser. Das Resultat von all diesem ist inzwischen ganz ordentlich.

Programm "VGA Indirect" Version 5

Damit war der 4 Takte/Pixel "Indirect" Algorithmus in der 3-Takte lpm Variante ausgereizt. Ziel ist aber der 3 Takte/Pixel "Indirect" Algorithmus in der 2-Takte ld Variante, und Font in SRAM. Also kommt jetzt als kleiner Zwischenschritt darauf vorbereiten. Das grösste Problem wird ab jetzt SRAM Mangel sein. Also ist 4+FBCOLS+2 Bytes an SRAM nur für den Stringbuffer auf die Seite legen schlecht, zumal das genau den 5+FBCOLS+1 Bytes einer Zeile entspricht, und damit eine Zeile kostet. Also wurden jetzt die String Sachen umgebaut, um direkt zu kopieren, was zur fünften Version führte.

Dazu wurden die STRBUF STRBUFLEN STRCONTLEN und vor allem STRDATA Variablen entfernt. Ebenso gibt es kein STRINIT:. Die STRLDI: und STRWRITE: wurden daher ohne Buffer dazwischen zusammengelegt, aber auch als FBWSTRI: wieder nach vorne gezogen. XYWSTR: nutzte STRWRITE:, neu nutzt es FBWSTRI:, also es umbenamst zu XYWSTRI:. Danach in Demo alle STRLDI: gefolgt von XYWSTR: zusammengelegt, da diese ohnehin immer in Paaren daherkamen.

In Gegensatz zu XYWSTRI: welches in rjmp FBWSTRI endet, benutzte XYWLINE: aber ein rcall XYWSTR, weil danach noch ein inc S0 kommt, was mit der ganzen "String nach call" Logik des FBWSTRI: unverträglich ist. Also alle STRLDI: gefolgt von XYWLINE: auch durch XYWSTRI: ersetzen, aber ein inc S0 danach hinsetzen, was ohnehin übersichtlicher ist.

Programm "VGA Indirect" Version 6

Damit war alles bereit um auf "Indirect" in der ld Variante umzustellen, was in der sechsten Version hier resultierte.

Weil nur noch 36x23 Zeichen ausgeben wurde der Timer von 40*12*(4/3)/8-1 = 74 auf 36*12*(4/3)/8-1 = 68 reduziert, und damit die Horizontalfrequenz leicht erhöht. Warum ich nicht einfach den Rücklauf ausgeben um 2*(2*24) vergrössert habe (allenfalls sogar mit darin Hintergrundfarbe ausgeben) ist unbekannt. Sicher ist dass dies zur Konstante TITOP = 68 führte. Auch wurden neu ROWSEGMENTS = 8 und SEGMENTLINES = 2 addiert. Bestehendes CHDRTROWS wurde von 25 auf 23 reduziert, CHDRGROWS sogar von 30 auf 23.

Speicherbenutzung hat es vor dem Font neu unbenutzte 32 Bytes, von 0x0060 bis 0x007F, in welche der Stack hineinkommt. Der Font in FONTRAM: selber bekommt für 128 Zeichen 8*128 Bytes, von 0x0080 bis 0x047F, ab 0x0480 frei lassend. Dabei werden FOCHARS = 0x80 und FOINDEXMASK = 0x7F definiert. Der Bildspeicher wird neu mit FBCOLS = 36 und obigem kleineren CHDRGROWS = 23 erzeugt. Eine Berechnung vom verbleibendem Platz wurde am Ende addiert, noch 2048 - 32 - 1024 - 966 - 2 = 24 Bytes an SRAM frei.

Wegen den wieder 3 Takten/Pixel hat es den neuen Algorithmus, mit dessen SEGM????: Routinen ab 0x0100, und nach ihnen allen anderen Programmcode ab 0x0200. FONT: wird neu zu FONTROM:, um es von FONTRAM: zu unterscheiden. Trotz nur 128 Zeichen SRAM platz wird weiterhin danach .org 0x0600 benutzt, was eigentlich 2kbyte|1kword für 256 Zeichen Flash belegt, und so auch weiterhin bis zu 256 Zeichen in vga_text_font.fon erlaubt. Neu sind FOCPCHAR: und FOINIT:, um Zeichen von FONTROM: nach FONTRAM: umzukopieren. Dabei wird auch die Verteilung von Videozeilen in Zeichen auf die 8 Fonttabellen gemacht. Das scheint nicht funktioniert zu haben, weshalb ich temporär einfach einen dummen Blockkopierer in FOINIT: gesetzt habe.

Das genfont blieb daher ähnlich, weiter mit Zeichen in 8 Blöcken, aber BG???? kamen wieder in 0x10..0x1F statt 0x80..0x8F, weil 0x80..0xFF nun weg sind. Die anderen Zeichen bleiben, aber werden nicht mehr kopiert. Neben Kommando NAME welches im .inc Zeilen mit .equ erzeugt, gibt es auch Kommando SECTION welches Labels generiert.

FBWCHAR: muss weiterhin nicht mehr auf kleiner niedrigstes testen, da dies bei 0 bleibt, aber wieder auf grösser oder gleich höchstes, da dies jetzt 0x7F ist. Bzw wird einfach mit andi T0,FOINDEXMASK alles 0x80..0xFF in 0x00..0x7F umgewandelt. Auf gleich ABORTPSEUDO muss weiterhin getestet werden. FBWSTRI: kann gleich bleiben, weil weiterhin ein reiner ASCII Bildspeicher, und FBWCHAR: den Bereich korrigiert. Ebenso bleibt FBINIT:, da es nur andere Konstantenwerte benutzt.

Alles bis und mit CHINIT: bleibt gleich, inklusive alles im vertikalen Rücklauf. Erst CHDRSTATE: wird mit neuem Algorithmus anderst. Erst ab CHFIRST: bzw eigentlich CHDOUBLE: wird es anderst. Das erste Zeichen vorbereiten muss wieder mit dem "parallel" vom neuen Algorithmus ablaufen. Davor müssen die Register dafür vorbereitet werden. Die ganzen CLUT Register bleiben, aber die XH:I0 müssen auf den Anfang der zur Zeile passenden Font Tabelle gesetzt werden, auf FONTRAM+CHSEGM*FOCHARS, und ebenso ZH auf high(SEGM).

Auch die Demo musste auf die 36x23 angepasst werden. DEBLANK: wurde systematisch zu DEBLANK_1X36:, und benutzt bestehendes S1 statt fest 0. DEASC_3X40: wird DEASC_3X36:, mit 2 statt 4 Blanks vor und nach den 32. DEBLACK: wird auch DEBLACK_1X36:. DERAIN_1X40: und DERAIN_6X40: werden aber DERAIN_1: und DERAIN_6:, weil sie keine spezifische Breite haben. DEWAVE_1X23: und DEWAVE_6X23: werden auf DEWAVE_1X19: und DEWAVE_6X19: gekürzt, ebenso DEGRAPH_6X40: auf DEGRAPH_6X36:. DEANN_3X40: wird zu DEANN_2X36. mit der dritten Zeile weg. DERGB_1X40: und DERGB_7X40: werden zu DERGB_1: und DERGB_7:, weil ohne Breite. DEMULT_1X40: und DEMULT_7X40: werden DEMULT_1X36: und DEMULT_7X36: mit mehrere "nor" (normal) weiter zu "no" gekürzt. DEWEB_2X40: wird DEWEB_1X36: mit der zweiten Zeile weg. DESCREEN: ist an alles angepast.

Auch die Animationen bekommen vergleichbares ab. DEROTBG_1X40: und DEROTBG_3X40: werden zu DEROTBG_1: und DEROTBG_3: ohne Breite. DESNAIL_1X40: wird DESNAIL_1X36:, von Zeile 22 zu 21 verschoben. Die Breite passt sich wegen FBCOLS automatisch an. DEBOFIELD_1X10: und DEBOFIELD_8X10: werden DEBOFIELD_1X8: und DEBOFIELD_6X8: mit BG???? Blockgraphik statt FS???? Zeichen, und ohne FS???? Rahmen, weil letztere nicht mehr in 128 Zeichen passen. DEBOBALL_3X3: wird wieder durch das alte DEBOBALL_AAA_3X3: bis DEBOBALL_BBB_3X3: wie in Version 1 ersetzt, weil FR???? Zeichen auch nicht in 128 passen. DEBOUNCE_8X40: wird DEBOUNCE_6X36: ohne den Rand löschen bzw nach zeichnen kompensieren. DECURSOR_3X40: wird DECURSOR_3X36: mit nur DEANN_2X36: statt DEANN_3X40: kompensieren, und Breite auch wegen FBCOLS automatisch. DEMO: ist auch an alles angepasst. RESET: hat Stack auf AFTERSTACK-1 setzen statt auf MAXRAM, und vor rcall FBINIT noch rcall FOINIT drin. Das Resultat von all diesem ohne Animationen laufend ist eine kleinere Demo.

Programm "VGA Indirect" Version 7

Das Hauptproblem mit SRAM Font und damit Platzmangel schlug prompt zu, in Form von einigen Abstürzen, wegen Stacküberlauf. Dieser muss aber in die erstes 32 Bytes vor dem Font passen, sonst sind diese verschwendet und Stack am SRAM Ende kostet eine weitere Zeichenzeile. Dies musste verbessert werden, was zur siebenten Version führte.

Ein grosser Stackverbrauch ist die Interrupt Serviceroutine, welche neben den unvermeidbaren Interrupt+reti 2 Bytes auch noch 7 Bytes SREG und XH:XL und YH:YL und ZH:ZL braucht. Diese wurden neu nach dem Bildspeicher als statische Variablen alloziert, die 24 freien Bytes auf 17 reduzierend. T0CMPINT: und DISPLAY: haben daher 7 sts statt push, ebenso BACKGROUND: 7 lds statt pop. Dies kostet zwar 7 Worte mehr aber keine extra Taktzyklen. Dabei wurde auch gleich die Reihenfolge des abspeicherns geändert, speichern von ZH bis XL und dann SREG, und wiederherstellen SREG und dann von XL bis ZH.

Der restliche vermeidbare Stackverbrauch ist in der Demo, viele call/ret und push/pop welche sich aufaddieren. DEASC_3X36: bekam statt push/pop S1 und absolutes ldi S1,2 relative subi S1,-2 und subi S1,2. DEGRAPH_6X36: sogar subi S1,-19 und inc S1 und subi S1,20. DESCREEN: verschwindet komplett, weil in DEMO: integriert um einen call/ret einzusparen, und damit konnten auch dessen push/pop S0 und S1 weg, mit so insgesammt 4 Bytes einsparen, bzw währed in DEASC_3X36: oder DEGRAPH_6X36: sogar 5.

Des weiteren wurde in SEGM0FXB und SEGM0BX3 das nop ersetzt durch ein zweites out VGADPORT,LUTBACK, mit dem nach Hintergrundfarbe austauschen dieses bereits in der Mitte vom Zeichen aktiv wird. Dies erlaubte endlich das "duplikate" SEGM1BXF und die darauf aufbauenden IVOFF und ULOFF ganz loszuwerden, sowie IVON und ULON in INV bzw ULIN umzubenennen. Dies erlaubte aber ebenso SEGM0FX3 durch SEGM0BX3 zu ersetzen, für eine zweite Hintergrundfarbe, aber dafür nur 3 statt 4 Vordergrundfarben, sowie Zeichen COL1 und COL2 und COL3 in FG1 bzw FG2 bzw BG3 umzubenennen. Dazu wurden auch der Font sowie genfont passend leicht angepasst. Und auch FBINIT: bekommt leicht andere Farben zum vorbesetzen.

DERGB: zeigt jetzt statt 2/3 hell und 1/3 hell und 2/3 pastell nun 2/3 hell und 2/3 pastell sowie Hintergrund 1/3 pastel. DEMULT_7X36: wurde zu DEMULT_8X36: welches diesen Effekt auch in den Standardfarben zeigt. Um Platz zu schaffen wurde DEANN_2X36: zu DEANN_3X36:, mit wieder 3 Zeilen ausgeben, dabei die Zeile vom wegfallenden DEWEB_1X36: dazunehmend, und so die Leerzeile vor DEWEB_1X36: einsparend. Das Resultat von all diesem ohne Animationen laufend ist eine optimierte Demo.

Programm "VGA Indirect" Version 8

Was in obiger Umstellung noch liegenbleib, war beim Font von Flash ins SRAM laden diesen umsortieren. Dort war immer noch der temporäre dumme Blockkopierer in FOINIT:. Dieser wurde jetzt behoben, was zur achten Version führte.

FOCPCHAR: wurde zu FOCHAR:, nutzt T0 statt S0, test zuerst auf gültig, rechnet dann erst FONTRAM+T0. Benutzt T1=ROWSEGMENTS..1 und T2 beim lpm. Verteilt die Zeichen von im FONTROM: linear zu im FONTRAM: in 8 Tabellen. Der lpm benutzt ,Z+ weil alle 8 Bytes eines Zeichens zusammen sind, aber der st nur X, mit danach subi/sbci weil es ja in 128er Schritten von Tabelle zu Tabelle muss. FOCHAR: korrigiert subi XL,-low(FOCHARS) zu subi XL,low(-FOCHARS), was vermutlich der Bug war wegen dem es erstmals nicht lief. FOINIT: hat den Blockkopierer nicht mehr, sondern benutzt FOCHAR: wie geplant. Das Zeilenabbruch Pseudozeichen wird direkt mit ldi XL,low(FONTRAM+ABORT) adressiert, statt mit ldi XL,low(FONTRAM) und dann subi XL,-ABORT. Dann wird direkt low(SEGMABRT) eingespeichert, egal was im Font im Flash drin war.

Um das Flash zu erzeugen wurde genfont vereinfacht, da es jetzt Zeichen in 256*8 statt 8*256 Anordnung erzeugen kann. Im Font wurden die 11 Zeichen 0x05..0x0F mit UN???? (UNused) benamst, um diese Namen später umbenennen zu können. Weiter wurden sie statt mit Leerzeichen mit erkennbaren Pixelmustern gefüllt.

Komplett neu ist CICHAR:, welches einzelne Zeichen aus dem Flash ins SRAM kopieren kann, mit nur Angabe welche SRAM Position 0x00..0x7E und welches Flash Zeichen 0x00..0xFF dorthin soll. Es setzt selber auf FOCHAR: auf, ähnlich wie alle XY*: Sachen auf FB*: aufsetzten.

In der Demo selber ist alles statische vom ehemaligen DESCREEN: identisch, ebenso die Animationen DEROTBG_3: und DESNAIL_1X36: sowie DECURSOR_3X36:, und auch DEMO: selber sowie RESET:. Aber der herumspringende Ball ist jetzt total anderst. DEBOFIELD_1X8: und DEBOFIELD_6X8: werden wieder zu DEBOFIELD_1X10: und DEBOFIELD_8X10:, wie sie in Version 2 bis 5 benutzt wurden. Nur mit anderen Zeichennamen, weil FS???? zwar im Flash Font einen festen Platz haben aber nicht im SRAM Font. Daher werden in DEBOFONT: die Flash und SRAM Positionen UN???? in DEBOFS???? umbenamst, und 6 der Flash Zeichen FS???? dorthin kopiert. Um FS0000 einzusparen wird identisches BG0000 benutzt. DEBOBALL_3X3: ist auch die Version 2 bis 5 Variante. Auch dazu werden 4 der Flash FR???? dorthin kopiert, insgesammt 10. Um FR0110 und FR1001 einzusparen weil nur 11 Platz werden FS0110 bzw FS1001 doppelt benamst. Das Resultat von all diesem inklusive alle 4 Animationen laufend ergibt die volle Demo, und wurde als drittes Video aufgezeichnet. Abgesehen von nur 36x23 statt den gewollten 40x25 (und davor ohnehin nur 30x25) kann es alles bisherige.

Programm "VGA Indirect" Version 9

Nach all diese wurde es erst möglich, den Vorteil des SRAM Fontes auszunutzen um Zeichen zur Laufzeit im Font umzuschalten, statt nur mit andere Zeichen in den Bildspeicher schreiben das Bild zu animieren. Damit wurden weit bessere Animationstechniken machbar, was zur neunten Version führte.

SEGM: und SEGM????: werden zu DRSEGM: und DRSEGM????:, weil sie die Segmente zeichnen. Die alten Namen werden frei um sie wiederzuverwerten, als Musternummer des Segmentes, ohne dessen Position in der Segmentroutinen Page zu kennen. Diese wird einer Flash Tabelle ab SEGM: entnommen, welche die low(DRSEGM????) Adressen drin hat, mit den SEGM????: Musternummern als deren Index. DRSEGMABRT: ist weder in der Tabelle drin, noch hat es einen SEGMABRT: Namen oder Index. SEGM????: mit ????=0000 bis 1111 sind mit 0x00..0x0F nummeriert, damit man direkt Bitmuster in Segmentmuster übersetzen kann. Die überflüssig gewordenen .org vor und nach FONTROM und .include "vga_text_font.inc" flogen raus.

Dazu wurde genfont erneut vereinfacht um nur noch SEGM???? Namen statt low(SEGM????) Adressen zu erzeugen. Gleichzeitig wurde es umgebaut um statt den ()..().. Mustern @@@@....@@@@.... zu benutzen, was einerseits dem "breiten" Font besser entspricht, und anderseits weit mehr Kontrast zeigt. Dies zeigt sich bei ASCII 0x30 so:

alter Syntax:           neuer Syntax:

CHAR ASCII "0" 0x30     CHAR ASCII "0" 0x30
........                ................
..()....                ....@@@@........
()..()..                @@@@....@@@@....
()..()..                @@@@....@@@@....
()..()..                @@@@....@@@@....
()..()..                @@@@....@@@@....
..()....                ....@@@@........
........                ................
    

FOCHAR: erwarten nun Indizes statt low(SEGM????), und testet diese auf unter SEGMNO = 0x14, da nur 0x00..0x13 gültig sind, mit alles andere auf SEGM0000 setzen. Dann wird Segmentnummer in der SEGM: Tabelle nachgeschaut, und das low(DRSEGM*) wie gehabt in den SRAM Font abgelegt. FOINIT: bleibt indentisch, bis auf eliminierte push/pop.

Neu kam als fünfte Animation die Ziffern 0..9 durch Demo-eigene "eckigere" Versionen ersetzen, wie oben gezeigt. Dazu wurden aber ab DESQUAREFONTROM: die neuen 0..9 direkt als SEGM???? Werte definiert, weil das inzwischen so einfach ist. Das eigentliche Programm bei DEFONT_0_9: verwendet den verbleibenden letzten Optionen Schalter PIND0. Es setzt stets zuerst den Standardfont, aber falls in Frames 32..63 von 0..63 blinkt es die zweite halbe Sekunde zum Demo-eigenen Font, so:

Standardfont:           Demo-eigene "eckigere" Version:

CHAR ASCII "0" 0x30     CHAR ASCII "0" 0x30
................        ................
....@@@@........        @@@@@@@@@@@@....
@@@@....@@@@....        @@@@....@@@@....
@@@@....@@@@....        @@@@....@@@@....
@@@@....@@@@....        @@@@....@@@@....
@@@@....@@@@....        @@@@....@@@@....
....@@@@........        @@@@@@@@@@@@....
................        ................
    

Programm "VGA Indirect" Version A/10

Obiges benutzte aber immer noch generische Graphikzeichen für den herumspringenden Ball, nur diese im SRAM Font einsetzend. Aber wirklich gut aussehende Graphik entsteht erst wenn man seine eigenen spezifischen Zellgraphik Zeichen benutzt, was zur zehnten Version führte.

Die ganze Infrastrktur blieb indentisch, lediglich CICHAR: verschwand, weil nicht mehr von DEBOFONT: benutzt. Alle anderen Änderungen sind in der Demo, und das auch nur in den Animationen, ab dem herumspringenden Ball, der vor allem komplett anderst wurde. Bisher hatten wir im zweiten Video zuerst mit fester ASCII Art plus BG???? Blockgraphik:

Ball 1 in ASCII Art:

   Zeichen 0,0     Zeichen 0,1     Zeichen 0,2
   000011112222333300001111222233330000111122223333
                  |               |               |
0  ...............|...............|...............|  Zeichen 0,2
1  ...............|...............|...............|
2  ...............|...............|...............|
3  ...............|...............|...............|
4  ...............|@@@@@@@@@@@@...|...............|  ASCII .-.
5  ....@@@@.......|...............|....@@@@.......|
6  ....@@@@.......|...............|....@@@@.......|
7_________________|_______________|_______________|
0  ...............|...............|...............|  Zeichen 1,2
1  ....@@@@.......|...............|....@@@@.......|
2  ....@@@@.......|...............|....@@@@.......|
3  ....@@@@.......|...............|....@@@@.......|
4  ....@@@@.......|...............|....@@@@.......|  ASCII | |
5  ....@@@@.......|...............|....@@@@.......|
6  ....@@@@.......|...............|....@@@@.......|
7_________________|_______________|_______________|
0  ...............|...............|...............|  Zeichen 2,2
1  ....@@@@.......|...............|....@@@@.......|
2  ........@@@@...|...............|@@@@...........|
3  ...............|...............|...............|
4  ...............|@@@@@@@@@@@@...|...............|  ASCII `-'
5  ...............|...............|...............|
6  ...............|...............|...............|
7_________________|_______________|_______________|


Ball 2 in BG???? Blockgraphik:

   Zeichen 0,0     Zeichen 0,1     Zeichen 0,2
   000011112222333300001111222233330000111122223333
                  |               |               |
0  ...............|...............|...............|  Zeichen 0,2
1  ...............|...............|...............|
2  ...............|...............|...............|
3  ...............|...............|...............|
4  ...............|@@@@@@@@@@@@@@@@...............|
5  ...............|@@@@@@@@@@@@@@@@...............|
6  ...............|@@@@@@@@@@@@@@@@...............|
7_________________|@@@@@@@@@@@@@@@@_______________|
0  ........@@@@@@@@...............|@@@@@@@@.......|  Zeichen 1,2
1  ........@@@@@@@@...............|@@@@@@@@.......|
2  ........@@@@@@@@...............|@@@@@@@@.......|
3  ........@@@@@@@@...............|@@@@@@@@.......|
4  ........@@@@@@@@...............|@@@@@@@@.......|
5  ........@@@@@@@@...............|@@@@@@@@.......|
6  ........@@@@@@@@...............|@@@@@@@@.......|
7__________@@@@@@@@_______________|@@@@@@@@_______|
0  ...............|@@@@@@@@@@@@@@@@...............|  Zeichen 2,2
1  ...............|@@@@@@@@@@@@@@@@...............|
2  ...............|@@@@@@@@@@@@@@@@...............|
3  ...............|@@@@@@@@@@@@@@@@...............|
4  ...............|...............|...............|
5  ...............|...............|...............|
6  ...............|...............|...............|
7_________________|_______________|_______________|
    

Dann kamen 256 Zeichen und erlaubten weit besseres im dritten Video und einem Photo, mit fester FR???? Rundeckengraphik:

Ball 3 in FR???? Rundeckengraphik:

   Zeichen 0,0     Zeichen 0,1     Zeichen 0,2
   000011112222333300001111222233330000111122223333
                  |               |               |
0  ...............|...............|...............|  Zeichen 0,2
1  ...............|...............|...............|
2  ...............|...............|...............|
3  ............@@@@@@@@@@@@@@@@@@@@...............|
4  ........@@@@@@@@@@@@@@@@@@@@@@@@@@@@...........|
5  ........@@@@...|...............|@@@@...........|
6  ....@@@@.......|...............|....@@@@.......|
7______@@@@_______|_______________|____@@@@_______|
0  ....@@@@.......|...............|....@@@@.......|  Zeichen 1,2
1  ....@@@@.......|...............|....@@@@.......|
2  ....@@@@.......|...............|....@@@@.......|
3  ....@@@@.......|...............|....@@@@.......|
4  ....@@@@.......|...............|....@@@@.......|
5  ....@@@@.......|...............|....@@@@.......|
6  ....@@@@.......|...............|....@@@@.......|
7______@@@@_______|_______________|____@@@@_______|
0  ....@@@@.......|...............|....@@@@.......|  Zeichen 2,2
1  ....@@@@.......|...............|....@@@@.......|
2  ........@@@@...|...............|@@@@...........|
3  ........@@@@@@@@@@@@@@@@@@@@@@@@@@@@...........|
4  ............@@@@@@@@@@@@@@@@@@@@...............|
5  ...............|...............|...............|
6  ...............|...............|...............|
7_________________|_______________|_______________|
    

Jetzt wird eine spezifische Zellgraphik benutzt. Weil es 11 UN???? Zeichen hat, wurde dazu eine 3x3=9 Zeichengruppe benutzt, was sich mit den bisherigen 3x3 Zeichen in der 6x6 Fläche deckt. In Vorbereitung auf die nächste Version wurden von den 3*4=12 Pixeln nur 3*4-(4-1)=9 benutzt. Daher kann der Ball 9 Pixel Durchmesser haben. Er wurde dann als eine solche Zeichengruppe definiert:

Ball 4 in Zellgraphik, statisch oben links im Feld positioniert:

   Zeichen 0,0     Zeichen 0,1     Zeichen 0,2
   000011112222333300001111222233330000111122223333
                  |               |               |
0  ............@@@@@@@@@@@@.......|...............|  Zeichen 0,2
1  ........@@@@@@@@@@@@@@@@@@@@...|...............|
2  ........@@@@...|........@@@@...|...............|
3  ....@@@@.......|............@@@@...............|
4  ....@@@@.......|............@@@@...............|
5  @@@@...........|...............|@@@@...........|
6  @@@@...........|...............|@@@@...........|
7__@@@@___________|_______________|@@@@___________|
0  @@@@...........|...............|@@@@...........|  Zeichen 1,2
1  @@@@...........|...............|@@@@...........|
2  @@@@...........|...............|@@@@...........|
3  @@@@...........|...............|@@@@...........|
4  @@@@...........|...............|@@@@...........|
5  ....@@@@.......|............@@@@...............|
6  ....@@@@.......|............@@@@...............|
7__________@@@@___|________@@@@___|_______________|
0  ........@@@@@@@@@@@@@@@@@@@@...|...............|  Zeichen 2,2
1  ............@@@@@@@@@@@@.......|...............|
2  ...............|...............|...............|
3  ...............|...............|...............|
4  ...............|...............|...............|
5  ...............|...............|...............|
6  ...............|...............|...............|
7_________________|_______________|_______________|
    

Dabei wurden um runder zu wirken die 8 Videozeilen/Zeichen ausgenutzt, auch wenn die Animation in der nächsten Version vertikal nur in 4 zweier Schritten bewegen wird. Diese Zeichen wurden in vga_debounce_ball.fon definiert. Um daraus ein vga_debounce_ball.inc zu erzeugen wurde genfont mit Filenamen Parameter erweitert, um beliebige *.fon in *.inc zu übersetzen. Das Makefile wurde auch angepasst um diese zu nutzen.

Ab DEBOBALLROM: wird mit .include "vga_debounce_ball.inc" dieses geholt. DEBOCHARS: lädt den nicht verschobenen Font ins SRAM, weshalb DEBOFONT: nicht mehr existiert, und CICHAR: nicht mehr benutzt wird. DEBOFIELD_1X10: und DEBOFIELD_8X10: wurden wieder durch DEBOFIELD_1X8: und DEBOFIELD_6X8: ersetzt, weil es zuwenige Zeichen frei hat um einen Rahmen zu zeichnen. DEBOBALL_3X3: verwendet einfach die 3x3=9 Zeichengruppe. Die restliche Logik ist in DEBOUNCE_6X36:, welches die 3x3 in die 6x6 hineinstellt, mit allen anderen normales Leerzeichen bzw BG0000, wie schon zu ASCII Art plus Blockgraphik bzw zu Rundeckengraphik Zeiten, in diesem Schritt erst zeichenweise positioniert. Nur die bessere Form ausnutzend.

DECURSOR_3X36: blieb wieder identisch. DEFONT_0_9: bekam ebenfalls seine Zeichen ab DEFODIGITSROM: mit .include "vga_defont_digits.inc" geholt, welche in vga_defont_digits.fon ausgelagert wurden. Zudem wurde der Font in Zeichen 0..9 einkopiern in DEFOCHARS: ausgelagert, was zwei mal benutzt wird.

Programm "VGA Indirect" Final

Obiges benutzte aber immer noch fertige Zellgraphik Zeichen für den herumspringenden Ball, nur diese im SRAM Font ersetzend. Aber wirklich gute Animation entsteht erst wenn man sogar pixelweise verschobene Zellgraphik Zeichen benutzt, was zur elften und finalen Version führte.

Das würde wenn nur fertige Zeichen benutzbar zu einer riesigen Menge davon führen. Bei je 4 Positionen horizontal und vertikal, müsste der 3x3 Zeichen grosse Ball 4*4*(3x3)=16*9=144 Zeichen bekommen! Dies kann man aber weil SRAM Font auch anderst erledigen, mit eine Gruppe von Zeichen vorzu erzeugen/modifizieren und dabei Graphikobjekte pixelweise verschoben in diese rendern. Die Basis dazu wurde bereits mit den SEGM???? = 0x00..0x0F schon gelegt, weil dies genau die 16 Pixelmuster binär erzeugbar durchnummeriert. Weil es horizontal 4 breite Pixel als Positionen hat, wurde die Ballgraphik auch vertikal auf 4 Zeilenpaare im Zeichen als Positionen ausgelegt. Da es somit je 4 Positionen einehmen kann, kann ein Graphikobjekt auf Zeichenzahl*4-(4-1) Pixel an Breite und Höhe ausgelegt werden. Daraus folgte die 3x3 Ball Zellgraphik auf genau 3*4-(4-1)=9 Pixel skaliert definieren, und nun so rendern:

Bewegung Zeilen 4 und 5 in vier horizontalen Positionen, in den drei Zeichen:

   Zeichen 0,0     Zeichen 0,1     Zeichen 0,2
   000011112222333300001111222233330000111122223333

4  |...@@@@........|...........@@@@|...............  Position 0
5  @@@@............|...............@@@@............

4  |.......@@@@....|...............@@@@............  Position 1
5  |...@@@@........|...............|...@@@@........

4  |...........@@@@|...............|...@@@@........  Position 2
5  |.......@@@@....|...............|.......@@@@....

4  |...............@@@@............|.......@@@@....  Position 3
5  |...........@@@@|...............|...........@@@@

Für nächste Positionen, mit viertes Zeichen aber kein erstes mehr:

4                  |...@@@@........|...........@@@@|...............  Pos 4 = 0
5                  @@@@............|...............@@@@............

4                  |.......@@@@....|...............@@@@............  Pos 5 = 1
5                  |...@@@@........|...............|...@@@@........
    

Damit folgt, dass man die Ballposition aufteilt, in Bit0..1 für pixelweises verschieben innerhalb der Zeichen, und Bit2..n für die Zeichen selber verschieben innerhalb vom Bildspeicher. Dies musste dann nur noch in der Demo umgesetzt werden. Da es bisher 3x3 Zeichen in 6x6 waren, und damit 4x4 machbare Positionen, werden es nun (4*4=16)x(4*4+16) davon. Also muss die Lissajou Figur Rechnung auf 4bit statt 2bit laufen.

FOCHAR: welches fest von Flash Font zu SRAM Font kopiert erwies sich als zu limitierend und wurde aufgespalten. Neu hat es nach SEGM: und SEGM????: aber vor FONTROM: noch SEGMCONV:, welches nur SEGM????: Nummer validiert und in low(DRSEGM????) wandelt, egal woher das Pixelmuster bzw die SEGM????: Nummer herkommt. Nach FONTROM: ist statt FOCHAR: neu FOLDCHAR:, welches den Resten macht, inklusive abspeichern ins SRAM.

Auch die Zeichen als Font laden erwies sich als ineffizient, weshalb in der Demo DEBOBALLROM: und DEBOCHARS: vor DEBOFIELD_1X8: verschwanden. Statt dessen wurde nach DEBOFIELD_6X8: ein schlecht benamstes DEBOBALLROM: addiert, in dem aber die Segmente zeilenweise drin sind, als 6 Blankzeilen plus 18 Ballzeilen plus weitere 6 Blankzeilen, zu je 3 Bytes. Damit kann man vertikal positionieren mit nur Anfangsadresse der benutzten Segmente um 0..3*(2*3) Bytes nach unten schieben.

In DEBOSHIFT: wird jetzt horizontal positioniert mit ein Segment nach rechts schieben, mit vorne unbenutztes vom letzten Segment addieren, und eigenes unbenutztes aufbewahren. DEBOLDROW: benutzt dies für alle 3 Segmente, sie so verlinkend und nach SEGMCONV: ins SRAM abseichern, das für 8 Zeilen. DEBOLOAD: benutzt dies für alle 3 Zeilen. Damit ist der Font fertig.

DEBOBALL_3X3: schreibt wie gehabt die Zeichencodes in den Bildspeicher. DEBOUNCE_6X36: spart etwas Prozessor mit nur DEWAVE_6X19: statt voll DEGRAPH_6X36: benutzen, errechnet Lissajou mit weniger der S2 Bits fallen lassen (nur 1 lsr T0 statt 3, und andi T0,0x1F statt 0x07) um 16 statt 4 Positionen zu haben, sowie die Bits aufspalten (die andi T0,0x03 vor call DEBOLOAD und 2 lsr T0 vor rcall DEBOBALL_3X3). Bei den "eckigeren" Ziffern wurde noch DEFOCHARS: in DEFOLOAD: umbenamst um konsistent zu sein. Das Resultat von all diesem inklusive alle 5 Animationen laufend wurde als viertes Video aufgezeichnet. Weit runder und flüssiger als noch im dritten Video.

Später wurde die SoftVGA an einer Ausstellung gezeigt. Da ich meinen 15" CRT nicht im Zug mitschleppen wollte, habe ich von einem Kollegen ein 8" LCD ausgeliehen. Leider wollte dies nicht synchronisieren. Als erstes verdächtigte ich das um 2*(8*2) Zeilen gekürzte Bild oder die um 4*(4*3) Takte gekürzten Zeilen. Also wurde dies korrigiert. Dazu wurde wieder TITOP = 74 benutzt, kein TITOP = 68 mehr. Das wurde dann kompensiert mit in CHDOUBLE: vor Zeichen ausgeben und DRSEGMABRT: danach, je 2*(4*3) Takte abwarten. Dabei wurde auch Register vorbesetzen vor CLUT laden umplaziert. Auch vertikal wurde korrigiert. Dafür wurden die VR[BT]B[TG]LINES: erweitert, bei den "T" um je 16, bei den "G" um je 56. Das ist die letzte Evolutionsstufe dieser Programmserie.

Wiederaufnahme nach 10 Jahren

Nach obigem Stand erreichen blieb das Projekt fast 10 Jahre liegen, von März 2009 bis Dezember 2018. In der Zeit wurden nur Ideen welche aufkamen notiert. Sowie 2 ATmega32 an einen Kollegen abgegeben und dafür 2 ATmega644 bekommen. Im Dezember 2018 wurde das Projekt wieder aufgeweckt, dessen Logfile mit allem was inzwischen geschehen war aktualisiert, und als Wiedereinstieg in den Programmcode der ganze Vortrag bis hierher geschrieben (und auch die beiden Vergleiche weiter unten). Kern der Weiterentwicklung wird die Umstellung um eine nun vorhandene AVR ATmega644 auszunutzen sein.

Als erstes kommen aber angesammelte Ideen welche noch auf dem ATmega32 laufen (und leicht modifiziert auf einem ATmega328 wie auf dem Arduino Uno benutzt laufen könnten), als letzte Evolutionsstufe mit diesem.

Als zweites kommen mit dem ATmega644 seinen 4k statt 2k SRAM volle 256 Zeichen SRAM Font und trotzdem vollen 40x30 Zeichen Bildspeicher, und damit alles was alle bisherigen Versionen konnten gleichzeitig.

Als drittes kommen mit dem ATmega644 seinen 24MHz statt 18.342MHz wählbare Videomodi, entweder 53 Zeichen zu 3Takte/Pixel mit dem bestehenden "Indirect" Algorithmus, oder 40 Zeichen zu 4Takte/Pixel bzw 32 Zeichen zu 5Takte/Pixel mit dafür aufwendigeren Fähigkeiten wie Zellfarben oder 2bpp oder gar beides davon.

Als viertes kommt anstelle der Demo die SoftVGA zur Ausgabehälfte eines Terminals erweitern. Dieses via PIO/PIA oder SIO/UART oder SPI mit ASCII und Kommandozeichen Codenummern ansteuerbar. Sei das für einen kleinen Homecomputer, oder nur für beliebige Elektronikprojekte als einfache PRINT Debugausgabe.

Als fünftes kommt Gegenstück eine SoftPS2 als Eingabehälfte des Terminals erzeugen. Dieses auch via PIO/PIA oder SIO/UART oder SPI mit ASCII und Statuszeichen Codenummern ansteuerbar. Sei das auch für obigen kleinen Homecomputer, oder nur für beliebige Elektronikprojekte als bessere Debugsteuerung.

Während als Test von AVR Hardware und PC Softwareinstallation mehrere alte Versionen laden, ist dem ATmega32 seine ISP Programmierschaltung gestorben, lieferte nur noch Bytes 0x53 zurück. Da ich ohnehin für den ATmega644 eine neue erweiterte 40pin AVR Experimentierplatine haben wollte, wurde diese nun hergestellt. Dazu wurde die Schaltung ihre Verdrahtung textuell beschrieben. Dabei auch gleich die des Programmieradapters und des UART zu RS232 Adapters, sowie der VGA Hilfsplatine und der DIP Schalter Hilfsplatine. Danach wurden die geplanten Verbesserungen der 40pin AVR Experimentierplatine designt, und auch gleich die 28pin AVR Version mit. Danach wurde die Platine hergestellt, bestückt, und getestet. Und dabei auch gleich der Programmieradapter mechanisch repariert. Und es ist mein einige Jahrzehnte alter Lötkolben gestorben.

Das war der Stand der Planung als ich diesen Vortrag Ende April 2019 am VCFe in München gehalten habe. Leider ist in den letzten 2 bis 3 Jahren die Situation im Internet massiv entgleist, mit Webadmins welche ohne Konsens der Benutzer das Internet in eine Hochsicherheitszone umbauen wollen, in der HTTP nicht mehr sein darf, nur noch neuestes HTTPS, und somit alle Software älter als 5 bis 10 Jahre (und Computer mit solcher) ausgeschlossen werden, und damit auch alle Retrocomputer (und auch Retrosoftware auf neueren Rechnern). Also ging das restliche Jahr drauf, mit eine Kampage aufbauen um diese Entgleisung zu reversieren. Hoffentlich wird jetzt ab Dezember 2019 endlich die Arbeit am Projekt weiter gehen können.

Uzebox als Vergleich

Das restliche Internet hat inzwischen nicht geschlafen. Die Uzebox Spielkonsole ist das beeindruckendste mir bekannte Projekt in dieser Hinsicht. Es wurde praktisch gleichzeitig wie die SoftVGA angefangen, und sogar vom selbigen Rickard Gunee PIC Game System inspiriert. Es erlebte aber keine fast 10 Jahre liegen gelassen werden, sondern wurde fertig gestellt und sammelte eine Community, welche es mit diversen Modi erweiterte, welche alle auf spezifische Spielesorten hin optimiert sind. Ich behandle hier aber nur die drei ersten vom Originator geschriebenen Videomodi 1 und 2 und 3, von denen der letzte ohnehin der am häufigsten benutzte ist.

Benutzt einen AVR ATmega644, der gnadenlos auf 2*14.318=28.636MHz (+43%!) übertacktet wird. Es erzeugt ebenfalls RGB, mit allen 3+3+2=8 Pins zu 8*8*4=256 Farben RRRGGGBB (bzw 3R3G2B), also was ich wegen verschmutzten Graustufen verworfen hatte. Er geht dann aber via einen AD725 RGB zu NTSC/PAL Analogcodierchip an einen NTSC Fernseher. Strikte kann dieser Chip auch PAL, aber nur wenn mit 17.734MHz statt 14.318MHz betrieben, was entweder nach 2*17.734=35.468MHZ (+77%!!!) verlangt, oder nach mit nur 17.734MHz untertakten, was aber auch nur 17.734/28.636 = 0.62 = 62% so viele Takte erlauben würde. Die EUzebox Variante meidet PAL durch SCART RGB erzeugen, läuft aber weiterhin mit NTSC Timing. Damit hat man entweder die Limiten von NTSC Farben verschmiert oder reine RGB Farben, aber letzteres nur falls SCART Fernseher (selten ausserhalb Europa) und dieser zudem NTSC Timing kann (nur bei neueren in Europa).

Damit ist die Uzebox rein NTSC, und bekommt so dessen anspruchloseres Timing, beinhaltend bis zu 51µs statt nur 25.4µs Zeit um zu zeichnen. Wegen 28.636MHz und horizontal 15.734kHz (63.5µs) verwendet er Timer 1 auf 1820 Takte/Zeile gestellt statt meinen 600, mit H-Sync Puls davon 63 Takte. Dessen Interrupt Serviceroutine testet auf erste Bildzeile, falls nicht erste wird nur der H-Sync Puls ausgegeben, gefolgt von reti, wie bei mir. Falls erster werden alle Bildzeilen ohne Pausen und reti gezeichnet, weil er keine Leerzeilen im Bild erzeugen muss, weil NTSC 240p statt VGA 400 oder 480. Damit gibt es auch keine Wahl von Leerzeilen streifig oder Doppelzeilen blockig (ist stets streifig). Zudem wird die Spiellogik auf den vertikalen Rücklauf limitiert, kann nicht bei streifig die Leerzeilen mit ausnutzen (weil dann stets voll Bild ausgebend ist).

Wegen SRAM Mangel setzt er ebenfalls auf Textgraphik statt Bitmap. Bzw strikter auch eine Zellgraphik, weil wegen Spielkonsole sein nicht mit ASCII vorbesetzt, sondern mit dedizierten Zellgraphik Zeichen für Graphikobjekte wie in den Modul Font ROMs von Konsolen. Diese werden wie dort als "Tiles" (= Kacheln) bezeichnet.

Uzebox Modes

Hier wurden inzwischen mehrere Programme bzw Modes implementiert:

Uzebox Mode 1

Der Mode 1 zeichnet 240x224 Pixel als 40x28 Zeichen zu je 6x8 Pixel, mit 6 Takte/Pixel. (Es gibt eine Modifikation davon, welche statt dessen 30x28 Zeichen zu je 8x8 Pixel macht, ebenfalls 6 Takte/Pixel.) Damit sind es 2*14.318/6=4.773MHz Pixel, unter einer C64 ihre 8MHz oder viele ihre 7.16MHz oder gar nur der NES ihren niedrigen 5.35MHz, aber über VC-20 bzw C64 2bpp ihre 4MHz. Mit den 240 Pixel sind das 50.3µs gezeichnet pro Zeile, und somit breiter als selbst die NES ihre grossen 47.8µs, bis voll in den Rand hinaus, nahe zu der Neo Geo ihre vollen 51.2µs, ebenso wie die 224 Zeilen fast voll in den Rand gehen. Will man Text benutzen, sollte es in etwa 40 bis 45µs bleiben, also passen dann noch 32 bis 36 Zeichen. Damit entspricht dies etwa einer TMS9918 mit den 32 Zeichen vom Graphikmodus, aber dem 6x8 Font vom Textmodus.

Damit hat Mode 1 pro Zeichen 6*(5+1)=36 Takte statt SoftVGA nur 4*(2+1)=12. Nach Abzug der 6 statt 4 out sind es sogar 30 statt 8. Dabei hat es pro Pixel ein lpm + out, welche zusammen direkt 8bit von Flash via Register zu Port kopieren. Was zu pro Zeichen 6*(2+3+1)=36 Takte führt, mit nach Abzug der lpm+out immer noch 6*2=12 um das nächste Pixelsegment "parallel" zu ermitteln und anspringen, statt nur 4*2=8 bei SoftVGA. Das mit pro Zeichen 8 Segmente zu 6 Pixel zu je 8bit ausgeben, also 48 Bytes im Flash, und somit pro 21.333 Zeichen 1kByte an Flash belegend. Kompakter als SoftVGA "Direct" pro 4 Zeichen 1kByte aber grösser als SoftVGA "Indirect" pro 128 Zeichen 1kByte.

Wegen diesem Format kann jedes einzelne Zeichen 48 beliebig farbige Pixel beinhalten, ohne jegliche CLUT oder Farbzellen. Aber daher kann man auch kein Zeichen in mehreren verschiedenen Farben darstellen, jedes muss in allen Farbvarianten als separate Zeichen drin sein. Colour cycling kann man als visuelle Technik praktisch vergessen. Anderseits kann dieser Modus wegen genug Takte volle 16bit Arithmetik mit Zeichennummern, aus nur (40x28)*2 = 2.2kBytes Bildspeicher im SRAM, und damit in den ganzen 64k Flash bis zu (64*1024)/48=1365 Zeichen benutzen, minus den Platz für die Software. Das Verhalten ist somit am ehesten wie bei einer Sega Master System. Das Resultat in mode1_loop: (in der Projektbeschrieb Doku auf Seite 16) sieht so aus:

                     ; {n}  in Kommentar = Takte benutzt, für Zeit rechnen

mode1_loop:          ;      dies alles geschieht jede Zeile, nicht pro Segment

lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 0 Farbe von Flash holen
                     ;        weil lpm nicht immer direkt vor out, so getrennt

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 0 ausgeben
ld r20,X+            ; {2}  nächstes Zeichen: Zeichenadresse (untere 8bit)
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 1 Farbe von Flash holen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 1 ausgeben
ld r21,X+            ; {2}  nächstes Zeichen: Zeichenadresse (obere 8bit)
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 2 Farbe von Flash holen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 2 ausgeben
rjmp .               ; {2}  unbenutzt: rest von Pixel abwarten
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 3 Farbe von Flash holen
                     ;        die 30x28 zu 8x8 Modifikation verdreifacht dies

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 3 ausgeben
add r20,r24          ; {1}  nächstes Zeichen: Zeichenadresse korrigieren
                     ;        welches der 8 Videozeilen * 6 Bytes (untere 8bit)
adc r21,r25          ; {1}    welches der 8 Videozeilen * 6 Bytes (obere 8bit)
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 4 Farbe von Flash holen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 4 ausgeben
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 5 Farbe von Flash holen
                     ;        dieses früher als sonst, weil ...
movw ZL,r20          ; {1}  nächstes Zeichen: ... ZH:ZL neu setzen
                     ;        mit dem nächten Segment für 6* lpm
dec r18              ; {1}    Anzahl verbleibende Segmente/Zeichen -1

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 5 ausgeben
brne mode1_loop      ;      nächstes Zeichen: entscheiden ob es eines hat
                     ; {2}    != 0: es hat noch Zeichen, weiter hinauf zu lpm
                     ;          wird mit diesem lpm {3} haben vor dem out
                     ; {1}    == 0: keine mehr, weiter geradeaus
rjmp .               ; {2}  unbenutzt: rest von Pixel abwarten
nop                  ; {1}    rest von Pixel abwarten
clr r16              ; {1}  rechter Rand: Farbe schwarz erzeugen

out VIDEO_PORT,r16   ; {1}  rechter Rand: für Rücklauf ausgeben
    

Die Kommentare stammen von mir, anstelle der originalen, um dies einfacher mit meinen "Indirect" mit lpm und "Indirect" mit ld vergleichen zu können. Wie man sieht, reicht die Zeit sogar um Zeichen pro Zeile in einer ganz normalen Schleife zu zählen, ohne ein Zeilenabbruch Pseudozeichen zu brauchen. Daher auch kein Tokeninterpreter, und kein mit ijmp weiterschreiten. Daher auch trotz Daten im Flash keine Kollision von ijmp mit lpm. Ebenso hat es keine LUTFORE oder LUTBACK Register, nur r16 als Zwischenspeicher. Mit den ld werden 2 Bytes Basisadresse nach r21:r20 geholt, und dann r25:r24 dazu addiert, welche je nach der Videozeile in der Zeichenzeile (0..7)*8 = 0,6,12,..,42 beinhalten. Dieses wird gerade vor der Schleife erzeugt, mit r22 0..7 und r23 Konstante 6 Pixel/Segment, durch mul r22,r23 und movw r24,r0. (Es gibt eine Modifikation um 0..n zu machen, mit dafür mehr Zeichenzeilen.) Auch hier wird voraus das erste Zeichen vor dieses ausgeben vorbereitet, mit nur den beiden ld und add bzw adc ohne lpd+out dazwischen. Zudem wird r18 mit der Konstante 40 Zeichen/Zeile geladen. (Die 30x28 zu 8x8 Modifikation macht dies 30.) Damit die selbige Zeichenzeile mehrere Videozeilen lang benutzt werden kann, braucht es noch ein movw XL,YL am Anfang, und YL muss alle 8 Zeilen modifiziert werden.

Uzebox Mode 2

Obiger Mode 1 hat selbiges Problem wie die SoftVGA hatte, bevor das mit "Indirect" aufkam, in der zweiten ld Variante, mit Font in SRAM ablegen. Jedes Zeichen hat eine fest in Flash gegebene Form (und hier sogar festen Farbsatz). Der Bildspeicher kann nur pro Zeichenposition eines dieser Zeichen anzeigen. Schlimmstenfalls gibt es nur ASCII Art, besser zumindest Blockgraphik oder gar sonstige Graphikelemente wie die Rundeckengraphik, bestenfalls ein spielespezifischer Font mit dedizierten Spielelement Zellgraphik Zeichen, wie im Modul Font ROM einer NES.

Wobei die Menge Zeichen schnell zur Limite wird. Ein nur 1x1 Zeichen grosses Spielobjekt (strikte passt 1.5x1.5 Zeichen gross) in 2 horizontalen und 2 vertikalen Postitionen zeichenbar, und damit über 2x2 Zeichenplätze verteilt, sind bereits 2*2*(2x2)=16 Zeichen, mit Leerzeichen wegoptimieren noch 1+2+2+4=9. Nur 4 horizontale und 4 vertikale Postitionen (und noch 1.25x1.25 Zeichen gross) macht das zu 4*4*(2x2)=64, optimiert 1+3*2+3*2+9*4=49. Nur 2x2 grosse Grupe (strikte 2.5x2.5) von Zellen in 2 hor und 2 ver braucht 3x3 Plätze und 2*2*(3x3)=36, optimiert 2x2+2x3+3x2+3x3=25. Sogar 2x2 gross (strikte 2.25x2.25) sowie 4 hor und 4 ver macht das 4*4*(3x3)=144, optimiert 2x2+3*2x3+3*3x2+9*3x3=121! Wie gezeigt im Hello World Beispiel, unter "Space Invaders Tiles", mit 1x1 in 2 hor und 2 ver.

Dem Spielobjekt seine Form varieren/animieren multipliziert die Zeichen. Es in eine Zeichenposition bringen welche auch Hintergrund drin hat, und die Menge explodiert. Auch bis zu 1365 Zeichen reichen bald nicht mehr. Daher sind auf diese Weise nur sehr statische Spielszenen machbar, wie Schach oder Maze oder Snake oder Tetris, oder aufwendiger Pac Man oder Pong, oder gerade noch Breakout oder obiges Space Invaders.

Es gibt dagegen ein paar Ansätze: Spielobjekt nur aus wenigen aber generischen Formelementen zusammensetzen, wie bei ASCII Art oder besser Blockgraphik oder gar sonstige Graphikelemente. Was aber schwach ist. Oder eine Bitmapgraphik benutzen. Was aber in der AVR ihrem kleinen SRAM nicht wirklich hineinpassen kann (160x100 liegen bei einer ATmega644 gerade noch drin). Oder ladbare und damit auch modifizierbare Fonts, wie ich es mit Font in SRAM ablegen bekam. Was aber auch SRAM Limiten hat. Oder in Hardware separat bewegbare Spielobjekte machen, also Sprites.

Mode 2 ist ein Versuch letzteres mit dem AVR zu machen. Der zeigt aber deutlich, warum das eine wirklich schlechte Idee ist. Als sichtbarstes hat Mode 2 nur noch extrem grobe 144x224 Pixel als 24x28 Zeichen zu je 6x8 Pixel, wegen riesigen 10 Takte/Pixel. Das sind es nur noch 2*14.318/10=2.8636MHz, selbst weit unter VC-20 bzw C64 2bpp 4MHz, nur noch knapp über VC-20 2bpp 2MHz. Diese 144 Pixel sind wieder die vollen 50.3µs ohne Rand. Text kann man so etwa 20 bis 22 Zeichen benutzen, also etwa VC-20, nur mit 6x8 statt 8x8@1bpp oder 4x8@2bpp.

Grund dieser massiven 10 statt 6 Takte/Pixel ist nur um die Menge an freier Takte von 6*(2+3+1=6)=36 auf 6*(6+3+1=10)=60 zu steigern, um so 6/2=3 mal mehr Zeit für "Planung" zu bekommen, um zu entscheiden ob ein Sprite oder Spielfeld Pixel jetzt dran ist. Dabei können 32 Sprites angemeldet sein (aber nur 31 davon nutzbar, einer muss voll transparent sein), mit pro Zeile maximal 5 davon anzeigen (ausser man geht auf Sprites rotieren hinaus, was aber flackert).

Dazu mussten noch um SRAM zu sparen für die Sprite Daten, die alten 16bit Zeichennummern aufgegeben werden, mit nur noch 8bit, und so nur noch 256 Zeichen, also 256 Form plus Farbe Kombinationen. Ich erachte das Resultat davon als genügend schlecht, dass ich den Programmcode dafür gar nicht erst angeschaut habe. Zumal Mode 3 weit besser ist, weil es auf Font teilweise in SRAM ablegen basiert, und so wieder mit 6 Takte/Pixel läuft.

Uzebox Mode 3

Mode 3 kann (224oder240oder256)x224 Pixel als (28oder30oder32)x28 Zeichen zu je 8x8 Pixel mit 6 Takte/Pixel. Es hat damit selbige 8x8 Zeichen wie der modifizierte Mode 1, was Text kostet mit nur noch 24 bis 27 nutzbar, aber weniger schlimm ist als Mode 2 seine etwa 20. Ebenfalls sind es nur noch 8bit Zeichennummern um SRAM Verbrauch zu halbieren, aber das kann mit den Font teilweise in SRAM etwas kompensiert werden, was hier als "RAM Tiles" bezeichnet wird. Es hat angeblich Sprites, trotz 6 Takte/Pixel, aber real sind diese nur mit SRAM Font automatisch generieren simuliert. Dies ist aber trotzdem der häufigst verwendete Modes, weil auch der universellste.

Der Grund für das "teilweise" ist, dass die Flash und SRAM Zeichen immerhin je 64 Bytes/Zeichen brauchen, im Gegensatz zur SoftVGA ihren nur 8 Bytes/Zeichen. Hatten letztere auf dem ATmega32 nur 128 Zeichen Font im SRAM, und auf dem ATmega644 volle 256, passen bei Mode 3 selbst auf dem ATmega644 mit nur 1k Bildspeicher maximal 3k/64 = 48 Zeichen Font ins SRAM. Was zuwenig ist. Daher wird mit einem Gemisch von SRAM plus Flash Zeichen gearbeitet, Nummern 0..n-1 in SRAM, und n..255 in Flash, mit n als Option setzbar, je nach wieviel SRAM man für Bildspeicher und Spiellogik noch braucht.

Die SRAM Zeichen kann man auf zwei Arten nutzen, manuell wie auf der SoftVGA (und auch auf einem Atari 800 oder TMS9918 oder VC-20 oder C64), oder automatisch mit einer Library welche sich benimmt wie wenn man Sprites hätte, aber real SRAM Zeichen alloziert und die "Sprites" in diese rendert, mitsammt allfälligem nicht verdeckten Spielfeld. Dabei können wegen den separaten 8bit Farben pro Pixel keine Attribute Clashes auftreten. Die Wahl trifft man aufgrund von ob die Spiellogik bereits SRAM Zeichen vorgibt, oder man lieber dem Prozessorverbrauch will von diese in jedem vertikalen Rücklauf neu rendern lassen.

Dazu muss der Segmentzeichner aber nun abwechselnd Flash mit lpm oder SRAM mit ld lesen, und das je nach Bildzeile in beliebiger Reihenfolge der benutzten Zeichen. Um dies effektiv zu machen, wird die Zeichenschleife dupliziert, zu romloop: und ramloop:, und je nach was das nächste Zeichen ist zwischen beiden hin und her gesprungen. Das Resultat (in der wie Videomodi funktionieren Doku) sieht so aus:

                     ; {n}  in Kommentar = Takte benutzt, für Zeit rechnen

romloop:             ;      dies alles geschieht jede Zeile, nicht pro Segment

lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 0 Farbe von Flash holen
                     ;        weil lpm nicht immer direkt vor out, so getrennt

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 0 ausgeben
ld r18,Y+            ; {2}  nächstes Zeichen: Zeichennummer (nur 8bit)
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 1 Farbe von Flash holen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 1 ausgeben
mul r18,r19          ; {2}  nächstes Zeichen: Nummer * (Breite*Höhe)
                     ;        mit Breite*Höhe einmalig gerechnet in r19
                     ;        Ergebnis in r1:r0, nicht in r18 welches bleibt
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 2 Farbe von Flash holen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 2 ausgeben
cpi r18,RAM_TILES_C  ; {1}  nächstes Zeichen: Vergleich Nummer < n
                     ;        also in Bereich 0..n-1, und somit in SRAM
in r6,SREG           ; {1}    Flags für später Carry extrahieren
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 3 Farbe von Flash holen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 3 ausgeben
brsh .+2             ;      nächstes Zeichen: Vergleich "Same or Higher"
                     ; {2}    C=0: in Flash, weiter wie bisher Flash Tabelle
                     ; {1}    C=1: in SRAM, auf SRAM Tabelle umstellen
movw r20,r4          ; {1}      benutze neu r5:r4, SRAM Tabelle Basisadr +Zeile
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 4 Farbe von Flash holen
   
out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 4 ausgeben
bst r6,SREG_C        ; {1}  nächstes Zeichen: extrahiere Carry zu T Flag
add r0,r20           ; {1}    addiere Tabelle Basisaddresse (untere 8bit)
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 5 Farbe von Flash holen
    
out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 5 ausgeben
adc r1,r21           ; {1}  nächstes Zeichen: Basisaddresse (obere 8bit)
dec r17              ; {1}    Anzahl verbleibende Segmente/Zeichen -1
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 6 Farbe von Flash holen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 6 ausgeben
lpm r16,Z+           ; {3}  aktuelles Zeichen: Pixel 7 Farbe von Flash holen
                     ;        dieses früher als sonst, weil ...
breq end             ;      nächstes Zeichen: entscheiden ob es eines hat
                     ; {2}    == 0: keine mehr, weiter zu schwarz ausgeben
                     ; {1}    != 0: es hat noch Zeichen, weiter geradeaus
movw ZL,r0           ; {1}  nächstes Zeichen Adresse bereitmachen

out VIDEO_PORT,r16   ; {1}  aktuelles Zeichen: Pixel 7 ausgeben
brtc romloop         ;      nächstes Zeichen: mit T entscheid welche Sorte
                     ; {2}    T=0: nächstes in Flash, weiter hinauf zu lpm
                     ; {1}    T=1: nächstes in SRAM, weiter geradeaus
rjmp .               ; {2}  unbenutzt: rest von Pixel abwarten

ramloop:             ;      nächstes SRAM fällt gleich hier durch
                     ;      vergleichbarer Code, aber mit ld+nop statt lpm
                     ;      und bei Nummer >= n wieder zu romloop
                     ;      mit statt r5:r4 SRAM Basis die r?:r? Flash Basis
    

Wie man sieht, hat es weit weniger warten darin, weil die hin und her Logik einiges braucht. Daher kann dieses auch nur mit 8x8 Pixel laufen und nicht mit 6x8, mangels Zeit dort. Die r21:r20 werden nicht mehr geholt, statt dessen muss r18 multipliziert werden und die neue r21:r20 Basisadresse dazu addiert werden, welche pro Zeile mit 0,6,12,..,42 zu den passenden Segmente verschoben ist. r5:r4 hat die derart korrigierte SRAM Basis drin, mir unbekannt wo die Flash Basis ist. Warum überhaupt r21:r20 Tabellen wechseln statt einfach direkt r5:r4 in SRAM bzw hier die unbekannten benutzen, seh ich keinen Grund. RAM_TILES_C ist die Konstante n, je nach wieviel SRAM man sich leisten kann. Diese wird fest hinein assembliert, weil man ja bei jedem Spiel eine Kopie der Ausgabe mitliefert, weil dies kein vom Spieleprozessor unabhängiger Videochip ist. Nach dem breq end muss wegen verfrüht herausspringen zuerst noch ein out VIDEO_PORT,r16 für Pixel 7 kommen, dann erst schwarz laden und warten und ausgeben.

Uzebox Andere Modes

Mode 4 wird beschrieben als Mode 3 mit 16x16 Zeichen, aber dafür 288x224 Pixel, wegen das 6/5 von 240 sein sind es wohl nur 5 der 6 Takte/Pixel, wohl weil die Logik auf 16 Portionen verteilt wurde.

Mode 5 wird beschrieben als Mode 1 mit nur 8bit Nummern wie Mode 2 und 3, rein um die Hälfte vom SRAM zu sparen.

Mode 6 wird beschrieben als nur SRAM Zeichen, bis zu 256 davon, aber dazu nur 1bpp, was an SRAM spart, nur 8 Bytes/Zeichen, alle 256 in 2k, wie es auch in der SoftVGA ist. Es hat weiterhin neben 240x224 (30x28 Zeichen) auch 288x224 (36x28 Zeichen) was auf 5 Takte/Zeichen hinweist, hier wohl wegen SRAM nur ld und niemals lpm. Weiterhin hat es nur bei 240x224 2 setzbare Farben pro Videozeile (nicht pro Zeichenzeile!), wohl weil bei 288x224 das SRAM nicht mehr dafür ausreicht.

Mode 8 ist ein reines Bitmap, zum Preis von nur 120x96 Pixel, wegen SRAM Mengenlimiten. Damit noch weniger als Mode 2, wirklich nur für Spiele welche sich nicht in Zellen zerlegen lassen. Weiterhin hat es auch nur 2bpp, mit 4 Farben CLUT, ohne pro Zeile setzen. Braucht somit wohl (120*8/2=30)x96=3kBytes SRAM.

Mode 9 wird beschrieben als "Code Tiles", bei dem die Pixelfolgen als Programmcode Schnipsel daher kommen. Hat höchste Auflösung, was auf geringste Takte/Zeichen hinweist. Submode "60" ist 360x240 aus 60x28 Zeichen zu je 6x8 Pixel, in 256 Farben pro Pixel. Submode "80" ist 480x240 aus 80x28 Zeichen zu je 6x8 Pixel, in 2 Farben pro Pixel. Es hat aber einen "sehr grossen" Programmspeicher Verbrauch, von 336 Bytes pro Zeichen. Dabei wird in "60" statt 3 Takte lpm einfach ein direktes 1 Takt ldi der 256 Farben verwendet, mit daher dem Code direkt zwischen diesen im Font in jedem Segment, analog zu wie ich es im "Direct" verwendet habe, mit dann ein ijmp zum nächsten, und dem Schleifenabbruch hier durch die ijmp Adresse überschreiben. Hat daher nur 4 Takte/Pixel. In "80" hat es nur 2 Farben und 4/3 mehr Pixel, also wohl 3 Takte/Pixel. Was nach noch näher zu meinem "Direct" sein klingt.

Mode 10 wird beschrieben als wie Mode 4 artig 12x16 plus wie Mode 5 nur Mode 1 artig mit 8bit, um intensiv SRAM zu sparen, nur 1k benutzt.

Mode 13 wird beschrieben als wie Mode 3, aber mit nur 32 Bytes/Zeichen weil nur 4 Bit/Pixel und eine Palette von 8 oder 15 Farben. Ist erst geplant.

Mode 40 wird beschrieben als bis 44x28 Zeichen, normal 40x25, mit 2kBytes pro Font, also 8 Bytes/Zeichen, mit Vordergrund und Hintergrund mit pro Zeichen Farbzellen, wie CGA. Mode 41 ist eine Variante mit nur Vordergrund pro Zeichen und Hintergrund pro Videozeile. Mode 42 ist eine Variante mit beides aus nur 16 Farben Palette. Mode 41 klingt sehr nach was ich im 1bpp Farbzellen Modus vor habe.

Gigatron als Vergleich

Der Gigatron ist eine TTL + 32kx8 SRAM + 64kx16 EPROM basierte Harward Architektur Microengine. Dieser generiert auch ein VGA Bild, im Graphik 480 Zeilen 60Hz Modus. Es hat einen 25.175MHz VGA /4 = 6.294MHz Takt. Es kann daher nur 1 Takt/Pixel nutzen, und bekommt so gleiche 160 Pixel wie die SoftVGA. Ebenfalls hat es die gleichen 64 VHRRGGBB Farben. Dieser kopiert dazu pro Zeile direkt 160*8bit an Bitmap von SRAM, pro Takt via ALU mit OR Akku (0xC0) für 6bpp RRGGBB und VH=11 zum Ausgang.

Das mit Speicheradresse autoincrement, aber nur von den unteren 8bit davon. Weshalb Zeilen nur 160von256 Bytes einer Page pro Zeile nutzen können, weil nur eine Zeile in 5/8 einer Page passt. Mit 128*256=32k SRAM passen so 120 Zeilen an VGA rein (mit 8 volle Pages = 2k unbenutzt), halb so viele Zeilen wie die SoftVGA erzeugt trotz weit mehr Speicher, weil sehr ineffizientes Bitmap. Diese Zeilen werden 1 bis 4 mal pro Bild gezeichnet, rest sind Leerzeilen, als Modi 1in4 2in4(=1in2) 3in4 und 4in4 Zeilen, halb so viel wie die 1in2 und 2in2 Zeilen der SoftVGA.

Ebenso verwendet dieser zwei Threads, Vordergrund VGA zeichnen und Hintergrund Rest wenn Leerzeilen, weshalb Modus 1in4 am schnellsten und 4in4 am langsamsten ist. Aber mangels Timer kann er keine Interrupt Serviceroutine für den Vordergrund benutzen. Man muss daher auch den Hintergrund komplett mit Zyklen abzählen schreiben, was mühsam ist. Weshalb als solches nur genau ein Programm läft, einen 16bit von Neumann Prozessor in vertikalem Mikrocode implementieren.

Dessen Befehlssatz hat weit unter 256 Befehle, weil dessen Opcodes schlicht die unteren 8bit der Adressen in einer Programmspeicher Page sind, wo alle Routinen anfangen welche die jeweiligen Befehle interpretieren. Alle Benutzer Software wird in dessen Programmcode geschrieben, in den verbleibenden 8*256+120*(256-160=96) Bytes Platz im SRAM. EPROM kann mit 8von16bit in den Microengine Akku gelesen werden, und so vom vertikalem Mikrocode als Code oder Daten benutzt werden. Das reicht um im 160x120@6bpp Bitmap ein Tetris zu spielen, einem mit NES Joypad am Eingang.


Home | Artikel | VCFe SoftVGA Interna

Diese Seite ist von Neil Franklin, letzte Änderung 2019.11.24