Der kleine Unterschied
Lasst uns mal kurz klären, wieviele Rasterzeilen wir überhaupt haben und wieviel Zeit uns (zumindest auf den ersten Blick) je Zeile zur Verfügung steht. Dabei kommen wir notgedrungen auch auf die Hauptunterschiede zwischen PAL und NTSC zu sprechen.
Ich möchte euch noch warnen, dieser Beitrag ist zwar kurz, aber auch relativ trocken, es wird gleich sehr theoretisch. Aber ohne Fleiß kein Preis, da muss man auf dem Weg zum Rasterzeilen-Profi einfach durch!
Wie immer ist das PAL-System unser Ausgangspunkt!
Wieviele Rasterzeilen gibt es überhaupt?
Da die Auflösung des C64 320*200 Pixel beträgt, könnte man nun dem Irrtum unterliegen, es gebe max. 200 Rasterzeilen. Aber zwei (euch eigentlich bereits bekannte) Punkte, sprechen dagegen. Zum einen gibt es den tollen Rahmen, der ja auch gezeichnet werden muss und das höchste Bit für die Rasterzeile in $d011 wäre ja nicht nötig, wenn es max. 256 Rasterzeilen gäbe.
Da wir gerne wissen möchten, wieviele Rasterzeilen es denn nun tatsächlich gibt, überlegen wir doch mal, wie wir diese ermitteln können. Ihr habt hoffentlich bereits den einleitenden Beitrag Der Rasterzeileninterrupt studiert, dann sollte euch direkt das Register $d012 einfallen.
Wir wissen bereits, dass der VIC die aktuelle Rasterzeile in $d012 mitzählt. Also können wir einfach schauen, was der höchste Wert in $d012 ist. Dabei ist es natürlich wichtig, darauf zu achten, dass auch das höchste Bit für die Rasterzeile in $d011 gesetzt ist, damit wir wirklich die max. Anzahl der Rasterzeilen zählen. Das folgende Programm ermittelt die Rasterzeilen und gibt diese als Hex-Wert oben links aus.
*=$0801 ;*** Startadresse BASIC-Zeile: 2018 SYS 2062 !word main-2, 2018 !byte $9e !text " 2062" !byte $00,$00,$00 main lda $d011 ;Warten, bis in $d011 and #%10000000 ;das höchste Bit gesetzt ist beq main ;... ldx #$00 ;Bei 0 beginnen loop cpx $d012 ;vergleiche $d012 mit X bcs loop ;falls kleiner, warten ldx $d012 ;aktuelle Zeile im X-Reg. merken lda $d011 ;solange in $d011 and #%10000000 ;das höchste Bit gesetzt ist bne loop ;wiederholen ;*** kurze Hexausgabe txa ;für Hexausgabe X in den Akku kopieren and #$0f tay lda hex,Y sta $0401 txa lsr lsr lsr lsr tay lda hex,Y sta $0400 rts ;zurück zum BASIC hex !scr "0123456789abcdef"
Wer möchte, kann zur absoluten Sicherheit noch die Zählroutine in einen sei / cli-Block verpacken, um Einflüsse von Außen zu vermeiden, eigentlich ist das aber nicht notwendig.
Wenn ihr das Programm auf einem PAL-System startet, dann sollte links oben 38 stehen (denkt daran, dass dies eine hexadezimale Zahl ist). Jetzt kennen wir die max. Anzahl der Rasterzeilen. Da das höchste Bit in $d011 gesetzt ist, müssen wir also einfach $0138 umrechen und kommen somit auf 312 Rasterzeilen. Beachtet, dass die Zählung bei 0 beginnt, die möglichen Zeilen reichen somit von 0 bis 311 (= $0137)!
Testen könnt ihr unser Ergebnis einfach mit einem kleinen Rasterzeileninterrupt:
*=$0801 ;*** Startadresse BASIC-Zeile: 2018 SYS 2062 !word main-2, 2018 !byte $9e !text " 2062" !byte $00,$00,$00 main sei ;IRQs sperren lda #<irq ;Adresse in den RAM-Vector sta $0314 lda #>irq sta $0315 lda $d011 ;höchstes BIT setzen ora #%10000000 sta $d011 lda #$38 ;gewünschte Zeile $100+<Wert> sta $d012 ;setzen lda #%00000001 ;Raster-IRQs erlauben sta $d01a lda #%01111111 ;Timer-IRQs sperren sta $dc0d ;die stören sonst nur lda $dc0d ;evtl. IRQ bestätigen lda $d019 ;evtl. Raster-IRQ bestätigen sta $d019 cli ;IRQs freigeben jmp * ;Endlosschleife irq ;Test-IRQ lda #%00000001 ;IRQ bestätigen sta $d019 inc $d020 ;Rahmenfarbe erhöhen pla ;Register vom Stack holen tay pla tax pla rti ;IRQ verlassen
So, wie das Programm hier vorliegt, sollte erstmal nichts passieren! Es hängt einfach in einer Endlosschleife fest, da auf die nicht vorhandene Zeile 312 bzw. $0138 gewartet wird! Nochmal zur Erinnerung: Die Zählung beginnt mit 0!! Sobald ihr aber die gewünschte Zeile für den Raster-IRQ um eins verringert (also #$37 nach $d012 schreibt), sollte der Rahmen (wie schon so oft) blinken.
Anzahl der Taktzyklen je Rasterzeile
Für einige Raster-Effekte ist es wichtig, Taktzyklen genau vorzugehen. Dazu müssen wir dann wissen, wieviele Zyklen in einer Rasterzeile verarbeitet werden. Dies läßt sich relativ einfach ausrechnen. Wie wir bereits wissen, wird der 6510 in einem PAL C64 mit 985248 Hz getaktet und pro Takt kann ein Befehl verarbeitet werden. Die Bildwiederholrate beträgt, wie uns ebenfalls bekannt ist, ~50 Hz. Da bei jedem Bildaufbau die 312 Rasterzeilen abgearbeitet werden, kommen wir auf 985248 / 50 / 312 = 63 (gerundet) Taktzyklen je Zeile. In einem der nächsten Beiträge werden wir sehen, dass es auch hier mal wieder einen Haken gibt, die 63 Taktzyklen stehen uns nämlich nicht immer zur Verfügung!
Und wie sieht es bei NTSC aus?
Sofern ihr WinVICE verwendet, können wir jetzt ganz einfach testen, wie das unter NTSC aussieht. Klick dazu im Menü auf Einstellungen -> C64 Modell Einstellungen… und stellt den Dialog, wie hier zusehen, ein:
Speichert anschließend eure Einstellungen und startet nochmal unser Testprogramm, das den Rahmen blinken läßt, mit dem höchst möglichen Wert für PAL-Systeme #$37. Wie ihr seht, seht ihr nichts 😉 .
Werft jetzt das Programm zum Zählen der Zeilen an und ihr solltet oben links eine 07 entdecken. Auf diesem NTSC-System haben wir also nur $0107 = 263 Rasterzeilen! Wie wichtig es ist, die Anzahl der Zeilen zu kennen, habt ihr eben ja schon selbst bemerkt. Würde euer Programm z. B. auf die Rasterzeile 300 warten, dann ist dies auf PAL-Systemen kein Problem. Wird es aber auf einem NTSC-System gestartet, läuft es nicht korrekt! Dort ist die Zeile unbekannt und es wird nie ein IRQ für die gewünschte Zeile ausgelöst.
Dass es weniger Rasterzeilen sind, sollte keinen überraschen, die physikalische Auflösung ist auf NTSC-Systemen schließlich geringer, die Bildwiederholfrequenz dafür mit ~60Hz um 20% höher.
Wie sieht es denn mit den Taktzyklen je Zeile aus?
Bei einem NTSC C64 ist der Prozessor bekanntlich schneller getaktet. Also rechnen wir dieses Mal 1022727 Hz / 60 Hz / 263 gerundet erhalten wir 65 Taktzyklen je Zeile. Wir haben also zwei Zyklen je Rasterzeile mehr zur Verfügung, hier gilt aber natürlich ebenfalls, dass diese nicht immer voll von uns ausgeschöpft werden können.
Ich hoffe euch ist aufgefallen, dass ich bei der Berechnung der Taktzyklen, sowohl unter PAL als, auch unter NTSC, jeweils gerundet habe. Dies liegt daran, dass wir bei unseren Berechnungen von einem falschen Wert ausgegangen sind. Der VIC-II liefert nicht exakt die von uns erwartete Bildwiederholrate! Diese ergibt sich aus den von uns ermittelten Werten. Wenn ihr nun die Bildwiederholfrequenz zurückrechnet, werdet ihr auf 50,124542 Hz bei PAL und 59,8260895 Hz bei NTSC kommen.
Zwei Sonderfälle
Um die Sache noch komplizierter zu gestalten, gibt es eine weitere NTSC-Version. In den ersten NTSC-Modellen war ein VIC-II mit recht verschrobenen Timing verbaut. Schaltet WinVICE doch mal auf die „alte NTSC“ Version um:
Startet ihr jetzt das Programm, um die Rasterzeilen zu zählen, seht ihr oben links eine 06. Bei den alten NTSC-Systemen, gibt es also nur $0106 = 262 Rasterzeilen. Wer jetzt wieder unsere obige Rechnung durchführt, kommt aber aufs falsche Ergebnis! Obwohl hier eine Rasterzeile weniger, als beim standard NTSC-Modell, vorhanden ist, erhöht sich die Zahl der Zyklen je Zeile nicht! Sie liegt mit 64 Taktzyklen sogar um eins niedriger!! Dies ist dem wohl nicht so optimalem Timing der ersten C64-Modelle geschuldet.
Als letztes möchte ich, nur der Vollständigkeit wegen, noch ganz kurz PAL-N erwähnen. Diese VICs wurden in den sog. „Drean Commodore 64“ verbaut. Drean war eine argentinische Firma, die schon 1983 (ganz offiziell) einen C64 unter eigenem Label auf den Markt bringen durfte. Die Geräte wurden von Commodore gefertigt und dann mit dem Drean Logo versehen.
WinVICE kann übrigens auch PAL-N emulieren, schaut ruhig mal nach. Unter PAL-N gibt es, wie beim normalen PAL-System, auch 312 Rasterzeilen, aber hier stehen uns je Zeile sogar 65 Taktzyklen (wie unter NTSC) zur Verfügung. Diese C64-Modelle haben somit einen Prozessortakt von ~1.01 MHz.
Was bedeutet dies alles jetzt für unsere Programmentwicklung?
Bei der Programmierung von Rasterzeilen-Effekten müssen wir evtl. auf die verschiedenen C64 Versionen Rücksicht nehmen. Wie man drei dieser Versionen erkennt, wisst ihr jetzt auch schon, jede hat ihre eigene Anzahl an maximalen Rasterzeilen, einzige Ausnahme ist PAL-N. Die Anzahl der Rasterzeilen läßt sich nicht nur so aufwendig, wie oben beschrieben ermitteln, sondern auch als kurzer Vier-Zeiler.
loop lda $d012 loop1 cmp $d012 beq loop1 bmi loop
Dieser kleine Abschnitt liefert euch im Akku die höchste Rasterzeile zurück. Man muss noch nur eins hinzuaddieren und erhält die gleichen Ergebnisse, wie bei unserer längeren Version von oben, die ja die max. Anzahl der Rasterzeilen geliefert hat. Auf einem PAL-System erhaltet ihr also $37. Addiert ihr dazu $100 für das höchste Bit aus $d011, landet ihr wieder bei $137 bzw. 311. Da die Zählung bei 0 beginnt müsst ihr noch 1 addieren, um wieder auf die oben bereits ermittelten 312 Zeilen zu kommen.
Probiert es ruhig mal aus…
Es kann durchaus sein, dass euch nicht direkt klar ist, wie und warum das hier funktioniert, schließlich beachten wir $d011 überhaupt nicht. Schaut euch dann einfach nochmal an, wie die Vergleichsbefehle arbeiten, spielt etwas damit herum und ihr werdet es verstehen.
Außerdem ist es evtl. wichtig, daran zu denken, dass die Bildwiederholfrequenzen unterschiedlich sind. Synchronisiert ihr etwas über den Raster-IRQ, dann wird dies bei NTSC (~60Hz) schneller vonstatten gehen, als unter PAL (~50 Hz). Häufig mag das kein großes Problem sein, man sollte es aber dennoch im Hinterkopf behalten.
Warum nicht $02a6 verwenden?
Wer sich bereits mit der Erkennung von PAL und NTSC-Systemen auseinandergesetzt hat, der ist evlt. schon über die Adresse $02a6 gestolpert. Bei NTSC sollte dort $00, bei PAL $01 stehen. ABER, der Wert wird nur durch die Reset-Routine gesetzt, da er sich im RAM befindet, kann er verändert werden. Bei einem RESET, während eine SuperCPU im 20MHz-Modus läuft, steht dort außerdem immer $00! Auch dies könnt ihr mit der WinVICE SuperCPU-Version testen. Laßt euch einfach mit ?PEEK(678) den Inhalt der Speicherstelle anzeigen, einmal nach einem RESET im normalen Modus und einmal nach einem RESET im 20MHz-Modus. Daher ist es sicherer, über die Anzahl der Rasterzeilen das vorliegende System zu erkennen. Der Grund, warum $02a6 hier falsch gesetzt wird, liegt ganz einfach daran, dass die SuperCPU, die ROM-Routine, die $02a6 füllt, viel zu schnell abarbeitet. Diese Routine versucht (ähnlich wie wir) die Fernsehnorm über die Rasterzeilenanzahl zu ermitteln. Sie geht aber davon aus, dass ein bestimmter Codeabschnitt länger als ein kompletter Bildschirmaufbau dauert (was bei einem normalen C64 auch der Fall ist). Aber diese Annahme führt dann, bei einer sehr viel schnelleren SuperCPU, zum falschen Wert. Mit 20MHz ist der ROM-Abschnitt schon erledigt, bevor die „PAL-Rasterzeile“ erreicht wird.
Wo wir gerade bei der SuperCPU sind, da die SuperCPU im 20MHz Modus Befehle schneller als der 6510 verarbeitet, können zyklengenaue Routinen dann natürlich aus dem Tritt kommen und der Effekt ist im Eimer.
Hier nun das Ergebnis unserer Arbeit.
Eine Tabelle, mit den wichtigsten Werten für PAL- und NTSC-Systeme.
Raster- Takt- sichtb. sichtbare VIC-II System zeilen zyklen Zeilen Pixel/Zeile ------------------------------------------------------- 6569 PAL 312 63 284 403 6567R8 NTSC 263 65 235 418 6567R56A NTSC 262 64 234 411 6572 PAL-N 312 65 ??? ???
Ich habe die Tabelle noch um die Anzahl der sichtbaren Zeilen und Pixel je Zeile ergänzt. Für uns sind die ersten beiden Eintragungen in der Liste sehr wichtig, diese Systeme werdet ihr „draußen“ häufig antreffen. Die letzten beiden (gelb markierten) sind wirklich sehr speziell, ob ihr eure Programme auch daran anpassen wollt???
Evtl. habt ihr den C64 DTV vermisst. Da dies kein echter C64 ist, habe ich ihn hier ausgelassen. Sein Timing ist außerdem sehr speziell 😉 . Wer möchte, kann es ja mal mit dem entsprechenden VICE-Programm (x64dtv.exe) untersuchen. Ich gehe evtl. später (SEHR VIEL SPÄTER) mal an gesonderter Stelle auf die DTV-Programmierung ein.
Da wir jetzt im Besitz des nötigen Wissens sind, werden wir uns im nächsten Beitrag eine recht simple Aufgabe stellen und tiefer in die Raster-Geheimnisse vordringen.
Hallo Jörn,
ich verzweifle an dem 4-Zeiler. Ich komme einfach nicht dahinter, warum das klappt.
Ich habe irgendwo einen Denkfehler, aber ich bekomme es nicht klar.
@loop lda $D012 ; Rasterzeile lesen – für das Beispiel angenommen Zeile 50 ($32)
@loop1 cmp $D012 ; vergleiche die eben gelesene Rasterzeile 50 mit dem aktuellen Rasterzeilenwert
beq @loop1 ; sind wir noch in Zeile 50 (also gleich), dann noch mal Rasterzeilengleichheit prüfen
; ist die nächste Raterzeile 51 erreicht läuft das Programm weiter
bmi @loop ; ist der Akkuwert negativ, dann gesamte Prüfung von vorne
; negativ wird der Wert ja erst ab Zeile 128, weil dann das hochwertigste Bit in $D012 gesetzt wird.
also warum bleibt in dem Beispiel nicht die Zeile 50 im Akku?
Liebe Grüße Jens
PS, bin mal gespannt, was in 8 Stunden und 18 Minuten passiert 🙂
Wäre schön wenn es was mit Assembler und Amiga zu tun hätte. Einen A500 habe ich nämlich noch.
wie geschrieben, ist die Funktionsweise des CMP, der Schlüssel.
Ohne jetzt direkt die Lösung preiszugeben, hier ein Hinweis, welche Annahme oben nicht ganz korrekt ist:
bmi @loop ; ist der Akkuwert negativ, dann gesamte Prüfung von vorne
Die Prüfung bezieht sich nicht direkt auf den Akku-Inhalt, sondern auf den Vergleich. Lies dir am besten nochmal die Beschreibung zum CMP durch. Schau dir anschließend das überschreiten der beiden interessanten Grenzen, wenn sich das höchste Bit in $D011 ändert an und was dort bei der obigen Prüfung genau passiert.
Direkt am Ende des Countdowns wird noch nichts passieren! Feier also lieber schön Silvester. Die Umstellung nimmt einige Stunden in Anspruch und zeitweise wird die Seite dann nicht mehr erreichbar sein!!
Ich denke mal gegen Nachmittag sollte das Meiste wieder laufen, sofern nichts schief geht.
Guten Rutsch,
Jörn
Hallo Jörn,
Danke, da habe ich meinen ersten Fehler erkannt. cmp setzt nicht nur die ZeroFlag! und beq sowie bmi beziehen sich beide auf die cmp Prüfung.
Mir fehlt aber zur Lösung noch etwas, denn jetzt sieht mein gedachter Durchlauf so aus:
Grenze 1 – Erster Durchlauf:
loop
lda $D012
; Rasterzeilenwert lesen – für das Beispiel angenommen 254 (Zeile 254)
loop1
cmp $D012
; vergleiche die eben gelesene Rasterzeilenwert 254 im Akku mit dem aktuellen Rasterzeilenwert 255 (Zeile 255) Ergebnis des Vergleichs = Negativflag auf 1, da Vergleichswert größer als der Wert im Akku ist, Zeroflag bleibt auf 0, da die Werte nicht gleich sind
beq loop1
; Zeroflag ist nicht 1, also Bedingung nicht erfüllt, kein Sprung, Programm läuft weiter
bmi loop
; Negativflag ist gesetzt, also sprung zu loop (gesamte Prüfung von vorne)
Grenze 1 – zweiter Durchlauf:
loop
lda $D012
; Rasterzeilenwert lesen – Wert ist 255 (Zeile 255)
loop1
cmp $D012
; vergleiche die eben gelesene Rasterzeilenwert 255 im Akku mit dem aktuellen Rasterzeilenwert 0 (Zeile 256 in der das höchstwertige Bit in $D011 zu 1 wird) Ergebnis des Vergleichs = Negativflag auf 0, da Vergleichswert kleiner als der Wert im Akku ist, Zeroflag bleibt auf 0, da die Werte nicht gleich sind
beq loop1
; Zeroflag ist nicht 1, also Bedingung nicht erfüllt, kein sprung, Programm läuft weiter
bmi loop
; Negativflag ist 0, also kein Sprung, Programm läuft zum Ende – im Akku müsste 255 stehen bleiben???
Grenze 2 – xter Durchlauf:
loop lda $D012
; Rasterzeilenwert lesen – Wert 55 (Zeile 311 letzte Zeile)
loop1 cmp $D012
; vergleiche die eben gelesene Rasterzeilenwert 55 im Akku mit dem aktuellen Rasterzeilenwert 0 (erste Zeile in der das höchstwertige Bit aus $D011 zu 0 wird) Ergebnis des Vergleichs = Negativflag auf 0, da Vergleichswert kleiner als der Wert im Akku ist, Zeroflag bleibt auf 0, da die Werte nicht gleich sind
beq loop1
; Zeroflag ist nicht 1, also Bedingung nicht erfüllt, kein Sprung, Programm läuft weiter
bmi loop
; Negativflag ist 0, also kein Sprung, Programm läuft zum Ende – im Akku müsste 55 stehen bleiben???
Liebe Grüße
Jens
PS: freut mich, dass es bei L.O.V.E. weiter geht und besonders, das das Amiga Projekt noch nicht abgeschrieben ist. 🙂
und ich habe das Umbauschild kurz nach Mitternacht gesehen 🙂
jetzt bin ich etwas überfragt, du kommst doch auf den korrekten Wert 55 bzw. $37.
Was fehlt dir denn noch?
Der Knackpunkt war für mich eigentlich, dass das Programm beim Wechsel von 255 auf 0 weiter in der Schleife bleibt, da $ff-0 negativ ist. Beim Wechsel von 55 auf 0 aber beendet wird, da $37-0 positiv ist.
Nur weil alle C64-Modelle über 256 und weniger als 384 Rasterzeilen haben, klappt diese Routine überhaupt.
Um 23:30 Uhr fiel mir noch ein, dass ich etwas als Hinweis auf den Wartungsmodus brauche. Das „Schild“ habe ich dann schnell mit PICO-8 erstellt.
Gruß,
Jörn
Hallo Jörn,
danke, das Du so viel Geduld mit mir hast.
Ich erhalte den korrekten Wert 55 bzw. $37, die Frage ist nur warum (Verständnisproblem).
Offensichtlich ist mir nicht klar, wie die Befehle cmp und bmi hier zusammen arbeiten.
Wenn ich es richtig verstanden habe, setzt cmp die Flaggen N Z C
Beim Vergleich Akku 255 und aktuellem Wert 0 – Vergleichswert ist kleiner als Akku – Folge N=0 Z=0 C=1
Beim Vergleich Akku 55 und aktuellem Wert 0 – Vergleichswert ist kleiner als Akku – Folge N=0 Z=0 C=1
Ich hatte jetzt angenommen, das der Befehl bmi auf die N-Flagge guckt und sieht nicht negativ – kein Sprung. Das scheint aber anders zu sein.
PS: Ich bin auf Deine Seite gekommen, als ich nach Hilfen für RetroPie gesucht habe. Dann bin ich über die Menge an Infos zur C64 Assemblerprogrammierung gestoßen und war angesteckt. Erst habe ich eine Menge gelesen. Dann mich im Rahmen eines kleinen Spielprojekts ans Programmieren gegeben. Das Thema RetroPie war bei mir schnell wieder vom Tisch. C64 Assembler scheint mich da mehr zu reizen 🙂
LG Jens
keine Sorge, frag ruhig nach, wenn etwas unklar ist.
Ich denke, du gehst davon aus, dass
CMP
sprichwörtlich schaut, ob 0 kleiner als 255 (bzw. besser $ff) ist und dann N, Z und C setzt.Wie im Beitrag erwähnt, wird von
CMP
aber intern eine Subtraktion ausgeführt.Die CPU rechnet also $ff – 0. Da im Ergebnis (es bleibt ja bei $ff) das höchste Bit gesetzt ist, wird auch N auf 1 gesetzt.
Daher springt das Programm in dem Fall per
BMI
wieder an den Anfang und fährt mit Zeile 256 bzw. 0 im Akku fort.Sobald Zeile 311 verlassen wird, zieht
CMP
von $37 wieder 0 ab. Dieses Mal wird das N-Flag aber nicht gesetzt, es bleibt ja bei $37 und dort ist das höchste Bit 0.Jetzt wird die Schleife verlassen und wir haben unser Ergebnis.
Gruß,
Jörn
Zweite Frage:
Das wir in diesem Beispiel alle Timer-IRQs deaktivieren, wäre es hier auch ok die irq: Routine mit “jmp $ea31” zu verlassen anstatt “pla, tay, …. , rti” , oder ist das eher nicht zu empfehlen?
Der Sprung nach
$EA31
ist durchaus möglich, da hier noch das ROM aktiv ist. Allerdings springt man in diesem Fall eher nach$EA81
, um auf denPLA ... RTI
-Block zu verzichten. Schau dir diese Adresse einfach mal mit dem VICE-Monitor an.Hallo,
Wieso bestätigen wir in diesem Beispiel zwei mal den Interrupt in $D019?
Hi.
Ich nehme an, du meinst das Bestätigen während der Initialisierung.
Es dient nur der Sicherheit, damit gleich der erste Raster-IRQ auf alle Fälle korrekt ausgeführt wird. Wirklich notwendig ist das hier aber nicht. Ich habe mir allerdings angewöhnt $D019 auch bei der Einrichtung eines Raster-IRQs zu bestätigen.
Super, alles klar, danke für die Antworten. 🙂