Eigentlich wollte ich ein Text-basiertes Spiel, das ich vor langer Zeit mal auf dem C64 in BASIC programmiert habe, auf einem ATMega644 zum Laufen bringen, nachdem ich es früher schon mal in C auf einen PC portiert hatte. Ich stellte aber fest, dass man ziemlich schnell an die Grenzen des Programmspeichers kommt (allerdings muss ich sagen, dass ich in dem C-Programm auch großzügig long Integers benutzt hatte, obwohl es oftmals auch kleinere Datentypen getan hätten). Da das Programm nicht zeitkritisch ist, entschloss ich mich, zur Reduzierung des Speichers einen Bytecode-Interpreter zu schreiben. Außerdem programmierte ich einen Compiler, der aus einer C-ähnlichen Programmiersprache Bytecode erzeugt. Genauer gesagt ist es kein eigenständiger Compiler, sondern durch C++-Klassen, Operatorüberladungen und einige #defines habe ich den C++-Compiler dazu überredet, aus einem C-ähnlichen Programm ein auf dem PC lauffähiges Programm zu erzeugen, welches nach dem Start schließlich Bytecode erzeugt. Eine Alternative wäre z.B. LCC oder ein Parser-Generator; da ich mit beiden jedoch keine Erfahrung habe, bemühte ich lieber den C++-Compiler. Leider entspricht die Syntax nicht 100 %ig ANSI C, v.a. bei der Definition von Variablen, Funktionen, structs usw.. Ausdrücke (mathem., logische) sind jedoch fast 100 % ANSI C kompatibel, weil man die C++-Operatoren so gut überladen kann. Einzige Ausnahme ist das x ? y : z -Konstrukt; man muss statt dessen cond(x,y,z) schreiben. Ich habe den Bytecode-Interpreter getestet, indem ich die float64-Bibliothek nach kleinen Syntax-Anpassungen mit dem Compiler kompiliert habe. (Fast) alle Funktionen der Bibliothek werden mit Zufallszahlen aufgerufen, und die Ergebnisse der unter avr-gcc kompilierten Funktionen werden mit den jeweiligen Ergebnissen der nach Bytecode kompilierten Funktionen verglichen. Bei irgendeiner Abweichung wird eine Fehlermeldung ausgegeben. Nach mehreren Stunden Laufzeit hat sich kein Fehler ergeben. Aber bei dem nicht gerade kleinen Programm (Compiler + Interpreter) werden sich leider durchaus irgendwo anders noch bugs versteckt haben. Für viele Anwendungen ist es jedoch zu langsam, wenn man die float64-Bibliothek auf dem Bytecode-Interpreter laufen lässt. Es werden ca. 20 Additionen und 12 Divisionen pro Sekunde ausgeführt. Sinus-Berechnungen und Konversion nach dezimal sind noch langsamer. Die reine float64-Bibliothek (ohne den Code zum Testen) hat einen Bytecode von ca. 5700 Bytes. Der Code wurde durch ein Kompressionsverfahren vom Compiler komprimiert und wird zur Laufzeit dekomprimiert. Winzip verringert in den Standardeinstellungen die Codegröße um ca. 11 % (aber der Bytecode-Interpreter kann Winzip gepackte Dateien nicht "on the fly" entpacken). Durch gewisse Verbesserungen am Komressionsverfahren könnte man jedoch den Bytecode noch um einige % kleiner machen (parameterbehaftete Code-Makros). Der Bytecode-Interpreter ist bereits in der Lage, parameterbehaftete Code-Makros zu entpacken; das entsprechende Kompressionsprogramm werde ich ggf. noch in den Compiler einbauen. Während die float64-Bibliothek als Bytecode wie gesagt ca. 5700 Bytes braucht, hat sie als C-Code nach Kompilierung mit avr-gcc ca. 26000 Bytes; somit ist das Verhältnis ca. 20-25 %. Bei anderen Programmen, die vorwiegend mit 8- oder 16-Bit Datentypen auskommen, wird die Codegrößenersparnis jedoch deutlich geringer sein, und wenn man die Größe des Bytecode-Interpreters (ca. 15000 Bytes) hinzurechnet, kann sich unterm Strich vielleicht gar keine Ersparnis ergeben. Wahrscheinlich ist der Einsatz des Bytecode-Interpreters nur bei Programmen sinnvoll, die viel mit 32 oder 64-Bit Datentypen rechnen. Denn ein 8-Bit Prozessor muss diese aus vielen 8-Bit Kommandos zusammensetzen. Beim Bytecode-Interpreter dagegen ist die Codegröße unabhängig von der Breite der Datentypen (wenn man mal davon absieht, dass 64-Bit Konstanten natürlich auch mehr Speicher als kleinere Konstanten benutzen). Der Bytecode-Interpreter wurde in Bezug auf kleinen Code optimiert, was auch zu Lasten der Ausführungsgeschwindigkeit geht. Für zeitkritsche Programme ist er daher nicht geeignet. Wenn jedoch nur Teile zeitkritisch sind, kann man sich eventuell dadurch behelfen, dass der Bytecode-Interpreter "externe" Funktionen aufruft, also Funktionen, die mit avr-gcc kompiliert sind und daher schneller sind. Momentan ist noch wenig dokumentiert. Je nach Zeit und Lust werde ich das ggf. in nächster Zeit nachholen. Wer jedoch mit C vertraut ist (und ggf. zumindest Grundkenntnisse in C++ hat), wird jedoch bei Betrachten des Quellcodes der float64-Bibliothek, den der Bytecode-erzeugende Compiler akzeptiert, schon mal einen Eindruck bekommen, wie die Syntax des C-Dialekts sich von ANSI C unterscheidet. Zusätzlich habe ich noch ein Hallo-Welt Programm beigefügt. Bei beidem Programmen gibt es jeweils einen Ordner "PC" und einen Ordner "ATMega644". Wenn man Änderungen am Quellcode durchgeführt hat, muss man zunächst die PC-Version öffnen (mit Visual Studio 2005/2008 oder mit gnu C++ (DevCpp)), den C++-Kompiler starten und dann das Programm ausführen. Der Bytecode wird aktuell in eine Datei "d:\program.txt" geschrieben; den Dateinamen kann mit in der Datei Main.cpp ändern. Den Inhalt dieser Datei (den Bytecode) muss man dann kopieren und ihn in das Array mem[] der ATMega644-Version einfügen (Ordner ATMega644, Datei main.cpp). Wichtig ist noch, die ggf. geänderte Adresse der auszuführenden Funktion in die Anweisung ip.execute_bytecode(...); (am Ende von main.cpp) einzutragen. Nun muss unter avr-gcc kompiliert werden, und nach dem Hochladen auf den ATMega sollte es hoffentlich funktionieren. Die Kompilierung dauert länger, wenn in der Datei BytecodeInterpreter.h das #define USE_CODE_MAKRO_COMPRESSION gesetzt ist; allerdings wird dann auch etwas kleinerer Code erzeugt. MS Visual Studio schafft es nicht (oder nicht in endlicher Zeit...), das Programm in der Release-Version zu kompilieren (genauer: zu linken), DEV CPP (gnu c++) dagegen schon. In der Release-Version ist die Kompilierung bei gesetztem #define USE_CODE_MAKRO_COMPRESSION deutlich schneller. Ein Nachteil des durch C++-Klassen realisierten Compilers ist, dass bei Fehlermeldungen nicht ohne weiteres die Zeilennummer ermittelt werden kann, in der der Fehler auftritt (da nützt auch das Makro _LINE_ nichts). Um dennoch zu erfahren, wo der Fehler auftrat, kann man in der C++-Entwicklungsumgebung in der Funktion report_error() der Datei BytecodeTargetedCompiler.h einen Breakpoint setzen und den C++-Compiler und dann das erzeugte Programm im Debug-Modus starten. Bei einem Fehler stoppt der Debugger dort, und wenn man in Call Stack etwas zurückgeht, kommt man an die Stelle, wo der Fehler ist. Aktuell ist aufgrund eines bugs bzw. eines noch nicht implementierten Details die Größe des Bytecodes auf 32 kBytes beschränkt. Es wird eine der nächsten Aufgaben sein, dies zu beheben, so dass prinzipiell auch deutlich größere Codes möglich wären (sofern genügend Flash vorhanden ist).
Warum nimmst du nicht den Maschinencode als Bytecode :-/
Weil der Maschinencode eben in solchen Fällen, wenn viel mit 32/64 Bit Datentypen gerechnet wird, deutlich mehr Speicher in Anspruch nimmt. Ein 8-Bit Prozessor wie ein ATMega muss z.B. eine 32 Bit Addition aus 4 8-Bit Additionen zusammensetzen. Der Bytecode-Interpreter hat z.B. einen ADD-Befehl, welcher die beiden obersten Stackelemente addiert und das Ergebnis wieder auf dem Stack ablegt. Die Codebreite des ADD-Befehls ist unabhängig von der Bit-Breite der Operanden.
Florian K. schrieb: > Der Bytecode-Interpreter hat z.B. einen > ADD-Befehl, welcher die beiden obersten Stackelemente addiert und das > Ergebnis wieder auf dem Stack ablegt. Die Codebreite des ADD-Befehls ist > unabhängig von der Bit-Breite der Operanden. Wie "mächtig" kann man sich die Maschine denn vorstellen an darstellbaren Operationen/Aktionen? Ist das eine Auswertung auf einem Kellerspeicher, also einem eigens reservierten RAM-Bereich, oder kann man damit ganze Programme formulieren inclusive Kontrollstrukturen, Prozeduraufrufen und Speicherzugriffen? Ist es möglich, über ein Interface "normale" C-Funktionen aufzurufen bzw. vom C-Code aus den Interpreter ein Schnippel Bytecode zu übergeben, ausführen zu lassen und Rechenergebnisse zu bekommen? Johann
Zur Info: Ich habe nun einen neuen Nickname (makrocontroller ==> LED). > Wie "mächtig" kann man sich die Maschine denn vorstellen an > darstellbaren Operationen/Aktionen? Die Mächtigkeit eines einzelnen Befehls ist vergleichbar mit einem Befehl eines 64 bit Mikroprozessors. Es sind arithmetische, logische Operatoren, bedingte/unbedingte Sprünge, Funktionsaufrufe usw. implementiert. Die Maschine arbeitet aber im Gegensatz zu einem Mikroprozessor Stack- und nicht registerorientiert. Eine Liste der Opcodes (Befehle) des Bytecode-Interpreters erhält man, indem man in der Datei BytecodeInterpreter.h nach enum Opcodes sucht. Nicht alle sind dokumentiert, aber teilweise selbsterklärend, weil vom Namen ähnlich wie Assembler-Befehle. > Ist das eine Auswertung auf einem Kellerspeicher, also einem eigens > reservierten RAM-Bereich, oder kann man damit ganze Programme > formulieren inclusive Kontrollstrukturen, Prozeduraufrufen und > Speicherzugriffen? Man kann ganze Programme ausführen, d.h. fast alles, was man in ANSI C programmieren kann, kann der Bytecode-Interpreter nach Compilierung auch ausführen (mit Ausnahme von z.B. Hardware-Zugriffen (Ports usw.); diese müssen durch Aufruf externen C-Funktionen erfolgen). Der ausführbare Code wird im FLASH abgespeichert und vom Bytecode Interpreter über die vom Benutzer übergebene Funktion(sadresse) get_code_nibble() ausgelesen. Der RAM wird nicht für Code, sondern nur für Daten verwendet, z.B. für den Stack. Der Benutzer übergibt beim Aufruf des Bytecode-Interpreters die Startadresse zweier Arrays im SRAM, welcher der Bytecode-Interpreter dann als Stack bzw. für globale Variablen nutzt. > Ist es möglich, über ein Interface "normale" C-Funktionen aufzurufen > bzw. vom C-Code aus den Interpreter ein Schnippel Bytecode zu übergeben, > ausführen zu lassen und Rechenergebnisse zu bekommen? Man kann sowohl von einem auf dem Bytecode-Interpreter laufenden Programm normale C-Funktionen aufrufen als auch umgekehrt, d.h. von einem C-Programm Funktionen des Bytecode-Interpreters aufrufen. Beim letzterem können momentan jedoch noch keine Funktionsargumente übergeben werden; es dürfte aber kein größerer Aufwand sein, dies noch einzubauen. Wenn man vom Bytecode-Programm normale C-Funktionen aufrufen will, muss dies momentan über eine Hilfsfunktion extern_function_invoker() erfolgen. Wenn der Bytecode-Interpreter den Opcode CALLEXT abarbeitet, ruft er die vom Benutzer bereitgestellte Funktion extern_function_invoker() auf und übergibt die Nummer der aufzurufenden C Funktion. Leider kann der Bytecode-Interpreter nicht selbst die C-Funktion aufrufen, weil es in C keine Möglichkeit gibt, eine Funktion mit einer erst zur Laufzeit bekannten Zahl von Parametern aufzurufen. Wenn jemand jedoch Erfahrung in AVR Assembler hat (im Gegensatz zu mir) und außerdem weiß, nach welchem Schema AVR-GCC die Parameter einer Funktion in Registern bzw. auf dem Stack übergibt, könnte er eventuell durch Assembler-Code erreichen, dass der Bytecode-Interpreter C Funktionen automatisch (ohne dem Umweg über extern_function_invoker()) aufruft. Momentan kann ein Bytecode-Programm mit einem normalen C Programm noch nicht über globale Variablen kommunizieren. Ich werde jedoch bei Gelegenheit in den Compiler einbauen, dass er automatisch #defines erzeugt, mit welchen man dann sehr einfach von einem C Programm auf die globalen Variablen zugreifen kann, die im Bytecode-Programm verwendet werden und die in dem Array memory[] abgespeichert werden, das beim Aufruf des Bytecode-Interpreters übergebenen wurde.
>Weil der Maschinencode eben in solchen Fällen, wenn viel mit 32/64 Bit >Datentypen gerechnet wird, deutlich mehr Speicher in Anspruch nimmt. Dafür ist es langsam... Also, es zwingt Dich niemand alle Befehle zu unterstützen. Bytecode kann ja schon Sinn machen, ich denke da auch an BASIC-Zeiten mit 1kByte RAM (PET, C64). Auch die FORTH-Sprache ist ein schönes Beispiel und warum unterstützt du nicht FORTH. Oder warum machst du dir nicht ein paar Makros für den Assembler, das geht doch auch. >Der Bytecode-Interpreter hat z.B. einen >ADD-Befehl, welcher die beiden obersten Stackelemente addiert und das >Ergebnis wieder auf dem Stack ablegt. Die Ergebnisse stehen so in Registern, das ist besser ->Harvard Architektur. Der Prozessor vom C64 ist ungeeignet für Compilersprachen, Bytecode für Basic die einzige Möglichkeit dem Benutzer das Ding schmackhaft zu machen. Skurril, Java hat einen JIT-Compiler: Eine Hürde bei den Mikros ist die fehlende Möglichkeit für selbstmodifizierenden Code, also im RAM kann kein Code stehen, was trickreich gelöst werden müsste. ->MikroVM und BASIC-Briefmarke?
... also im RAM kann kein Code stehen ... Es gibt Controller die Code im RAM ausführen können (z. B. 51er, ARM7).
> Leider kann der Bytecode-Interpreter nicht selbst die >C-Funktion aufrufen, weil es in C keine Möglichkeit gibt, eine Funktion >mit einer erst zur Laufzeit bekannten Zahl von Parametern aufzurufen. Kennst du die Ellipse (...) ? Also ich hatte schon mal einen C++ Compiler gebaut, der das konnte was du möchtest. Es gab mehrere Möglichkeiten der Codeerzeugung. Compilat und Skript konnten immer transparent kommunizieren, das war erste Bedingung und auch der Zweck des Ganzen, leider ist das nicht "bulletproof", also der Computer konnte damit abstürzen. Ich habe es über Exceptionhandling abgefangen. Bei der Entwicklung hatte ich LISP im Hinterkopf, es gab eine EVAL-Funktion (Sprache Groovy, PHP) Einzelne Skriptfunktionen konnten per Compilerschalter ist 3 Modi erzeugt werden: - Abarbeitung des RDP-Tree im Skriptmodus (genausoschnell wie Java) - Erzeugung von VM-Code (Bytecode) mit der Möglichkeit das zu speichern - Erzeugung von Maschinencode - nicht optimiert Weil ich eigentlich helfen möchte finde ich du solltest besser gleich_irgendeinen_ Standard unterstützen (Java, Javascript, PHP, FORTH, Basic, C) sonst wird das ein Rohrkrepierer. Du könntest z.B. GCC dazu bringen deinen Bytecode zu erzeugen, das wäre dann für einen großen Nutzerkreis und auch nicht mehr auf Mikros beschränkt. Der Aufwand hält sich - vermutlich gegen deine Erwartungen - in Grenzen. Gut die 2 Teile müssen sich schon etwas auf einander zubewegen aber dafür wirfst du die Last des Compilerbaus ab. Leider liegt mein Projekt schon mehr als 15 Jahre zurück und der Markt hatte es damals nicht angenommen, die Nutzung blieb auf ein Projekt beschränkt.
Ich verwende einen C-Dialekt (als Quellsprache für den Compiler, der Bytecode erzeugt), weil ich mich mit C gut auskenne und ein bereits existierendes C Programm speichersparend auf einem µC zum Laufen bringen will. Wer sich mit Forth gut auskennt, wird wahrscheinlich Forth bevorzugen. Zu anderen Vor- und Nachteilen von Forth vs. C kann ich nichts sagen, da ich Forth nicht kenne. Der Bytecode-Interpreter implementiert nicht alle Befehle irgendeines Mikroprozessors (weder vom C64 noch von irgend einem andereren), sondern den Vergleich habe ich nur gemacht, um eine Vorstellung über die "Mächtigkeit" eines Befehls zu vermitteln. Der Bytecode-Interpreter ist tatsächlich langsam. Dies hat verschiedene Gründe. Allgemein habe ich kleinen Code gegenüber höhere Geschwindigkeit vorgezogen, weil ich von vornherein auf nicht zeitkritische Anwendungen abzielen wollte. Es wäre sicher auch möglich, einen Bytecode-Interpreter mit ähnlichem Befehlssatz mehr auf Geschwindigkeit statt auf Codegröße zu optimieren, um ihn z.B. an einem µC zu betreiben, welcher Code asynchron (d.h. nicht synchron mit dem Bustakt) aus einem externen Flash liest. Beim Test der float64-Bibliothek habe ich ermittelt, dass ein einzelner Befehl des Bytecode-Interpreters durchschnittlich ca. 3000 Prozessortakte in Anspruch nimmt. Das erscheint schon sehr hoch. Ein Grund ist sicher auch, dass parallel zu Daten (Variablen) auch der Typ der jeweiligen Variable im SRAM abgespeichert wird, um den Typ nicht jedes Mal im Code angeben zu müssen. Vorteil ist kleinerer Code, Nachteil geringere Geschwindigkeit und genau 25% mehr Speicher für (lokale) Variablen, die auf dem Stack gespeichert werden. Globale Variablen brauchen jedoch nicht mehr SRAM Speicher, weil der Typ meist aus der Adresse ermittelt werden kann (außer bei globalen struct-Objekten). Aber wenn jemand Interesse an meinem Bytecode-Interpreter hat, könnte er ggf. die Geschwindigkeit erhöhen, indem er Teile davon in Assembler schreibt (ich habe keine Erfahrung mit AVR Assembler). Und der Interpreter ist ja vom Codeumfang deutlich "übersichtlicher" als der Compiler. Die Ellipse (...) für Funktionsargumente kenne ich. Man kann sie aber nur nutzen, um innerhalb von Funktionen wie z.B. printf eine zur Laufzeit unbekannte Zahl von Argumenten auszulesen. Aber wenn die Argemente und Datentypen einer Funktion z.B. in einem Array stehen (Anzahl und Datentypen zur Comiplierzeit unbekannt) und die Adresse einer Funktion bekannt ist, kann man der Funktion zwar die Adresse des Arrays übergeben, sie aber nicht mit den "richtigen" Parametern aufrufen. Hört sich nicht schlecht an, dass du schon mal einen C++-Compiler geschrieben hast. Zum Theme GCC: Ich stelle es mir sehr aufwändig vor, mich erst mal in den Quellcode von GCC einzuarbeiten, um ihn dann dazu zu bringen, Bytecode zu erzeugen. Das hätte dann allerdings den Vorteil, dass man sogar C++-Code kompilieren kann. Ich gehe mal davon aus, dass es weniger Aufwand machen würde, einen Compiler mit einem Parser-Generator zu schreiben. Hast Du Erfahrung mit Parser-Generatoren und kannst Du einen empfehlen ? Florian
>Hört sich nicht schlecht an, dass du schon mal einen C++-Compiler >geschrieben hast. Ist nicht wesentlich anders als C. Die Funktionen haben eine Signatur, d.h. die Argumente sind im Namen codiert. Es gibt zusätzliche Prioritäten/Regeln für die Abarbeitung, die für den Menschen offensichtlich schwieriger zu durchschauen sind als für ein Programm. Manche behaupten sogar der Compiler "denkt". Mein Ding war unter Borland C++ lauffähig unter Visual C++, gcc müsste man viel überarbeiten, praktisch neu schreiben. >Zum Theme GCC: Ich stelle es mir sehr aufwändig vor, >mich erst mal in den Quellcode von GCC einzuarbeiten, um ihn dann dazu >zu bringen, Bytecode zu erzeugen. Da kommt man nicht drumrum... >Das hätte dann allerdings den Vorteil, >dass man sogar C++-Code kompilieren kann. Das ist nicht unbedingt ein Vorteil. Ich sehe C++ nur als Verkleidung und versuche soweit wie möglich alles in C zu halten. Beispielsweise läuft die Überladung des operators [] auf eine entsprechend benannte C-Funktion, "funktionale Programmiersprachen" lassen grüssen. >Ich gehe mal davon aus, dass >es weniger Aufwand machen würde, einen Compiler mit einem >Parser-Generator zu schreiben. Das ist schon richtig. So macht es gcc und mit zusätzlichen Konstrukten. Auch awk, perl und wie sie alle heissen. Viel schlimmer ist, keine Sau hat sich mein durchaus interessantes Teil auch nur angeguckt. Zugegeben damals gab es noch kein Internet so wie heute und Java hiess noch Oak, Konkurrenzprodukte gab es schon jeher (USCD-Pascal). >Hast Du Erfahrung mit Parser-Generatoren >und kannst Du einen empfehlen ? ->lex, flex, bison, byacc, gibt es auch lauffähig unter Windows. Die sind schon eine Erleichterung. Ich habe aber nur einen kleinen, eigenen "recursive descent parser" eingesetzt. Die Compilerbauertools sind multimegabyte gross und dienen auch mitunter zu Benchmarkzwecken. Schau Dir auch mal "CINT" an. Das gab es mal unter Linux. Ich sehe jetzt gerade läuft das unter der Bezeichnung "ROOT" von Masaharu Goto. Da kannst du mal sehen wovon es abhängt, wie weit sich ein Produkt entwickelt. Deine Sache ist schon interessant ich muss es mir demnächst genauer anschauen.... Gruss
Mitesser schrieb: > Weil ich eigentlich helfen möchte finde ich du solltest besser > gleich_irgendeinen_ Standard unterstützen (Java, Javascript, PHP, FORTH, > Basic, C) sonst wird das ein Rohrkrepierer. > > Du könntest z.B. GCC dazu bringen deinen Bytecode zu erzeugen, das wäre > dann für einen großen Nutzerkreis und auch nicht mehr auf Mikros > beschränkt. Der Aufwand hält sich - vermutlich gegen deine Erwartungen - > in Grenzen. Gut die 2 Teile müssen sich schon etwas auf einander > zubewegen aber dafür wirfst du die Last des Compilerbaus ab. Florian K. schrieb: > Die Ellipse (...) für Funktionsargumente kenne ich. Man kann sie aber > nur nutzen, um innerhalb von Funktionen wie z.B. printf eine zur > Laufzeit unbekannte Zahl von Argumenten auszulesen. Aber wenn die > Argemente und Datentypen einer Funktion z.B. in einem Array > stehen (Anzahl und Datentypen zur Comiplierzeit unbekannt) und die > Adresse einer Funktion bekannt ist, kann man der Funktion zwar die > Adresse des Arrays übergeben, sie aber nicht mit den "richtigen" > Parametern aufrufen. > > Hört sich nicht schlecht an, dass du schon mal einen C++-Compiler > geschrieben hast. Zum Theme GCC: Ich stelle es mir sehr aufwändig vor, > mich erst mal in den Quellcode von GCC einzuarbeiten, um ihn dann dazu > zu bringen, Bytecode zu erzeugen. Das hätte dann allerdings den Vorteil, > dass man sogar C++-Code kompilieren kann. Ich gehe mal davon aus, dass > es weniger Aufwand machen würde, einen Compiler mit einem > Parser-Generator zu schreiben. Ein gcc-Port kam mir auch gleich in den Sinn; daher auch meine Frage nach der "Mächtigkeit" des Instruktionssatzes. Für gcc scheint alles vorhanden zu sein: -- Load/Store/Move (zwischen Speicher und/oder Registern, Konstanten, Adressen) -- Arithmetik (Shift, Plus, Minus, Vergleich) -- Logik (And, Or, Xor, Not) -- Bedingte und unbedingte (direkte und indirekte) Sprünge (Branch, Goto, Labels) -- Call (direkt, indirekt)/Return -- Sign-Extend, Zero-Extend (zb von 8-Bit Wert zu 16-Bit Wert) Allerdings geht gcc von einer Registermaschine aus, nicht von Befehlen, die auf einem Stapel operieren. Wenn man weiß, wie man's anzupacken hat, bekommt man einen null-gcc (also einen, der eine leere Funktion übersetzen kann) schon innerhalb einer Woche. Für eine erste funktionieren Version meines letzten gcc-Ports brauchte ich rund 3 Wochen, also mit überschaubarem Aufwand. Für ein Hobby-Projekt muss man aber schon was an Zeit einplanen, und im ersten Schritt wär's ja auf jeden Fall was Hobby-mässiges. Die gcc-Quellen muss man dazu nicht kennen, wesentlich ist aber jemand zu haben, der einem Sachen erklären kann. Die Grundlagen dazu, wie ein Port zu geschehen hat, sind dokumentiert in den Internals http://gcc.gnu.org/onlinedocs/gccint/ die leider fern davon sind, vollständig zu sein. Der erste Schritt eines solchen Ports wäre es, sich zu überlegen, wie die "Maschine" aussehen soll: Registersatz, ISA (Instruction Set Architecture) d.h. Befehlssatz, Adressierungsarten, etc. Gcc zu porten hätte folgende Vorteile: 1) Man braucht sich nicht mit Parser auseinandersetzten. 2) Man muss sich nicht mit Sprachen wie C++ und deren Fallstricken und Gemeinheiten ausseinandersetzen 3) Man bekommt optimierten (Byte)code! 4) Man kann die ISA so entwerfen, daß sie schmackaft für gcc ist. Stolpersteine, die reale Architekturen immer bereithalten, kann man vermeiden. 5) Die Libc, Libm etc. sind in C geschrieben und könnte übernommen werden! Dito für andere Bibliotheken! 6) Man lernt viel über GCC. Nicht unbedingt von Nachteil... Compilerbauer sind gesuchte Fachkräfte (auch in der Krise) Und Nachteile 1) Es ist aufwändig. Allerdings wurde in dein Projekt auch einiges an Arbeit reingesteckt, wenn man so durch die Quellen blättert. 2) GCC gest davon aus, daß er Code für einen Assembler erzeugt. *) 3) bc-gcc hätte die selben Probleme mit der Harvard-Architektur wie avr-gcc 4) Nicht zu vernachlässigende Wahrscheinlichkeit, daß das Projekt versackt. ad *) Das bedeutet zB, daß bc-gcc (der hypotethische Bytecode-gcc), Module erzeugen wird. Ein solches Modul kann unaufgelöste Referenzen enthalten wie etwa in der C-Quelle
1 | extern int i; |
2 | extern int bar (int); |
3 | |
4 | int foo (void) |
5 | {
|
6 | return bar(i); |
7 | }
|
bzw, dem daraus hervorgehenden Objekt. Für die C-Quelle würde der bc-Assembler etwa so aussehen. Das wäre die Ausgabe, die bc-gcc schreibt. Also noch kein fertiger Bytecode:
1 | .section .progmem.data,"a",@progbits |
2 | .global foo |
3 | foo: |
4 | load.16 R0, i ; Lade i aus dem RAM in Register R0 |
5 | return |
Hier müsste also ein Reloc für die Adresse von i erzeugt werden. Mit einem bc-as würde man diesen lesbaren Bytecode-Assembler umformen in "normalen" AVR-Assembler. Weiter geht es dann mit den bekannten avr-tools (Assembler, Linker) und das so erhaltene Modul wird zu der Anwendung hinzugelinkt:
1 | .section .progmem.data,"a",@progbits |
2 | .global foo |
3 | foo: |
4 | .byte ??? ; Opcode für load.16 R0, * |
5 | .word i ; avr-as erzeugt den Reloc: avr-ld trägt zur Linkzeit |
6 | ; die Adresse von i (16 Bits) ein |
7 | .byte ??? ; Opcode für return |
Man könnte diese Opcodes auch in bc-gcc ausdrücken, aber eine lesbare asm-Datei möchte man schon... Johann
Mitesser schrieb: > Die Ergebnisse stehen so in Registern, das ist besser ->Harvard > Architektur. Ich habe mir auch Gedanken über Vor- und Nachteile von Registern bzw. Stack gemacht. Ich habe mich aus folgenden Gründen für eine Stack-orientierte Maschine entschieden: -- Ausdrücke (mathematische, logische usw.) sind hierarchisch organisiert. Daher erscheint es "natürlich", die Zwischenergebnisse auf dem Stack abzulegen. -- Wenn man v.a. Wert auf kleinen Code legt, ist man mit einer Stack-orientierten Maschine unter Umständen besser dran: Man braucht nicht bei jeder Operation anzugeben, aus welchen Registern die Operanden gelesen werden sollen, sondern sie werden implizit oben vom Stack geholt. Bei einer Registermaschine werden sich aber oft Befehlssequenzen ergeben, in denen ein Ergebnis in einem Register abgelegt wird, und im nächsten Befehl wird wieder aus dem gleichen Register gelesen ==> die Nummer des Registers muss doppelt abgespeichert werden. -- Wenn man einen Compiler selbst schreibt, ist es wahrscheinlich viel schwieriger, ihn für eine Registermaschine zu optimieren. Die Verwendung von Registern entspricht nicht der natürlichen Hierarchie, mit der Ausdrücke aufgebaut sind. Der Compiler muss immer "wissen", welche Register überschrieben werden dürfen und bei welchen der Inhalt vielleicht noch für später benutzt werden kann. Und beim Aufruf einer anderen Funktion müssen manche Register auch auf den Stack "gerettet" werden. Wenn man keine virtuelle Maschine hat, sondern einen Prozessor, dann mag es sein, dass durch die Verwendung von Registern vielleicht schnellerer Code erzeugt werden kann. Aber natürlich kann meine Maschine nicht nur jeweils auf das oberste Stackelement zugreifen, sondern auch auf tiefere Elemente, z.B. auf lokale Variablen, die im Stack gespeichert werden. Und es kann auf globale Variablen zugegriffen werden. Register sind sozusagen globale Variablen; meine Maschine kann bei Bedarf also auch eine den Registern entsprechende Funktionalität nutzen. Ich habe momentan zwar nicht so viel Zeit, aber bei Gelegenheit werde ich mal einen Blick auf die Anleitung für einen gcc-Port werfen oder mich auch mit Parser Generatoren näher beschäftigen.
Florian K. schrieb: > Mitesser schrieb: >> Die Ergebnisse stehen so in Registern, das ist besser ->Harvard >> Architektur. hmmm. Das hat doch nix mit Harvard oder nicht-Harvard zu tun. Harvard sagt was darüber aus, wie auf peicher zugegriffen werden kann bzw. wie adressiert werden muss (aus Compiler-Sicht) > Ich habe mir auch Gedanken über Vor- und Nachteile von Registern bzw. > Stack gemacht. Ich habe mich aus folgenden Gründen für eine > Stack-orientierte Maschine entschieden: > -- Ausdrücke (mathematische, logische usw.) sind hierarchisch > organisiert. Daher erscheint es "natürlich", die Zwischenergebnisse auf > dem Stack abzulegen. > -- Wenn man v.a. Wert auf kleinen Code legt, ist man mit einer > Stack-orientierten Maschine unter Umständen besser dran: Man braucht > nicht bei jeder Operation anzugeben, aus welchen Registern die Operanden > gelesen werden sollen, sondern sie werden implizit oben vom Stack > geholt. Bei einer Registermaschine werden sich aber oft Befehlssequenzen > ergeben, in denen ein Ergebnis in einem Register abgelegt wird, und im > nächsten Befehl wird wieder aus dem gleichen Register gelesen ==> die > Nummer des Registers muss doppelt abgespeichert werden. Schon. Wenn man bei einer realen Maschine mal ausrechnen würde, wieviel in einem Programm nur zur Codierung der Register-Nummern verbraucht wird, käme man auf nen recht hohebn Anteil: Bei AVR codiert ne Adition Rn += Rm in 16 Bits; 10 Bits davon müssen n und m codieren. Dito für ADD, SUB, CMP, XOR, ... Allerdings würde ich schätzen, ein agressiv optimierender Compiler auf Register-Basis schneidet besser ab als ein nicht-optimierender auf Stapel-Basis. > -- Wenn man einen Compiler selbst schreibt, ist es wahrscheinlich viel > schwieriger, ihn für eine Registermaschine zu optimieren. Die Verwendung > von Registern entspricht nicht der natürlichen Hierarchie, mit der > Ausdrücke aufgebaut sind. Der Compiler muss immer "wissen", welche > Register überschrieben werden dürfen und bei welchen der Inhalt > vielleicht noch für später benutzt werden kann. Und beim Aufruf einer > anderen Funktion müssen manche Register auch auf den Stack "gerettet" > werden. Ja. Register-Allokierung ist ein sehr komplexer Teil eines Compilers. Zumindest dann, wenn es gut machen und Register gut auslasten will (wie zB GCC). Wenn man keinen Wert darauf legt wie BASCOM, dann ist's simpel bzw. garnicht vorhanden. Reale Maschinen helfen sich was Codedichte angeht oft mit mehreren Ausprägungen einer Instruktion. ZB gibt es dann eine 4-Byte Addition Ra = Rb + Rc aber für den Fall, daß 2 Register übereinstimmen, gibt es eine 2-Byte Addition Ra += Rb Oder es gib Opcodes, in denen ein bestimmtes Register implizit codiert ist; sowas wie ein Akkumulator. Oder es gibt Opcodes für kompexere, oft auftauchende Funktionalitäten wie Ra = Rb*Rc + Rd Ra = MAX (Rb,Rc) > Wenn man keine virtuelle Maschine hat, sondern einen Prozessor, dann mag > es sein, dass durch die Verwendung von Registern vielleicht schnellerer > Code erzeugt werden kann. Reale Prozessoren haben num mal Register, und daher wird versucht, diese möglichst gut auszunutzen. Speicher für lokalle Variablen soll so weit als möglich vermieden werden, weil die Zugriffe Platz und Zeit kosten, und die Variable RAM belegt. Daher ist GCC zB so heiß drauf, lokalen Variablen den Garaus zu machen. Dennoch bestehen die meisten Programme zum Großteil aus Speicherzugriffen und wenig komplexen Ausdrücken. Sehr komplexe Ausdrücke gibt es bestenfalls in Mathe-Algorithmen. Etwa wenn man ein Blick in die Berechnung der Γ-Funkton in der libm wagt. > Aber natürlich kann meine Maschine nicht nur jeweils auf das oberste > Stackelement zugreifen, sondern auch auf tiefere Elemente, z.B. auf > lokale Variablen, die im Stack gespeichert werden. Und es kann auf > globale Variablen zugegriffen werden. Register sind sozusagen globale > Variablen; meine Maschine kann bei Bedarf also auch eine den Registern > entsprechende Funktionalität nutzen. > > Ich habe momentan zwar nicht so viel Zeit, aber bei Gelegenheit werde > ich mal einen Blick auf die Anleitung für einen gcc-Port werfen oder > mich auch mit Parser Generatoren näher beschäftigen. Was ist das eigentich für eine Software, für der der Interpreter geschrieben wurde? Wär auch mal interessant zu wissen, wie die Größenverhältnisse an (Binär)Code sind für -- Interpreter -- Bytecode -- Native C-Routinen Johann
Johann L. schrieb: > Allerdings würde ich schätzen, ein agressiv optimierender Compiler auf > Register-Basis schneidet besser ab als ein nicht-optimierender auf > Stapel-Basis. Ein paar einfache Optimierungen führt auch mein Compiler durch. Sicher macht da GCC viel mehr. Aber ein nicht unerheblicher Teil der Optimierungen von GCC wird wohl der Ausnutzung der Register dienen, was bei einer Stapel-Maschine entfällt. Zusätzlich kann mein Compiler bestimmte Optimierungen machen, die GCC vom Prinzip her nicht kann: Code-Makros + Huffman-ähnliche Codierung der oft benötigten Opcodes mit kleineren Bit-Längen. Und wie erwähnt, schneidet mein Compiler bei 32- oder 64-Bit-Datentypen besser ab, weil die Operationen nicht aus vielen 8-Bit Operationen zusammengesetzt werden müssen. Aber wenn ich eine Register-Maschine implementieren würde, könnte ich manche Optimierungen ebenfalls nicht so gut wie GCC durchführen, aber die anderen weiterhin. Ich gehe davon aus, dass sich eine Register-Maschine nur bei einem GCC-Port lohnt (wenn fertige Teile von GCC die Optimierungen schon durchführen), nicht jedoch bei einem selbst geschriebenen Compiler. > Was ist das eigentich für eine Software, für der der Interpreter > geschrieben wurde? Ein Text-basiertes Spiel ("Finanzspiel") für 2 oder mehr Personen, das ich mal auf dem C64 in BASIC geschrieben habe. Man kann Rohwaren einkaufen, diese zu Fertigwaren produzieren, Häuser und Grundstücke kaufen, diese hoffentlich teurer verkaufen oder vermieten u.a. Und manches wird in Dollar abgerechnet; manchmal muss man Dollar in gute alte DM umtauschen, und hoffentlich bei einem möglichst hohen Dollarkurs (welcher abhängig ist von der Zahl im "Markt" vorhandener Dollars). Wer am Ende am meisten Geld hat, gewinnt. Sicher, dieses Programm ist nicht unbedingt der einzige Grund für mich, einen Bytecode-Interpreter zu schreiben. Vielleicht könnte ich das Spiel auch als normalen C-Code in 64K unterbringen, wenn ich mehr 8- oder 16-Bit Datentypen verwenden würde. Aber mir ging es auch darum, mal etwas prinzipielle Erfahrung mit der Programmierung eines Bytecode-Interpreters zu sammeln. > Wär auch mal interessant zu wissen, wie die Größenverhältnisse an > (Binär)Code sind für > -- Interpreter > -- Bytecode > -- Native C-Routinen Das habe ich oben mal angedeutet: Der Bytecode-Interpreter braucht ca. 15000 Bytes - je nach Konfiguration mehr oder weniger. Wenn man z.B. die avr-gcc-floats benutzen will, muss man die float(32 Bit) Bibliothek hinzurechnen. Meine float64-Bibliothek (komplett mit allen Funktionen) braucht als Bytecode etwa 5700 Bytes, während sie als compilierter normaler C-Code etwa 26000 Bytes braucht. Die Größeeinsparung wird bei anderen Programmen, die mehr mit 8- oder 16-Bit-Datentypen rechnen, wahrscheinlich deutlich geringer sein.
Um wenigstens mal einen Anhaltspunkt zu erhalten, wie die Codegrößenverhättnisse zwischen normalem kompilierten C Code und meinem Bytecode sind, wenn ein Programm vorwiegend 8- oder 16 Bit-Datentypen benutzt, habe ich einfach mal in der float64-Bibliothek alle 64- und 32-Bit Variablen mit der Replace-Funktion des Editors in 16 Bit Variablen umgewandelt und außerdem 0x durch (uint16)0x ersetzt, wodurch die meisten 32- oder 64 Bit-Konstanten ebenfalls kürzer werden. Natürlich tut das Programm dann nichts sinnvolles mehr. Avr-gcc liefert dann ein Kompilat von ca. 8600 Bytes, und mein Bytecode ist wie erwartet nur geringfügig kleiner als in der 64-Bit-Version, nämlich 5150 Bytes. Aber der Bytecode ist in diesem Fall immer noch ca. 40 % kleiner als der Maschinencode. Danach habe ich noch alle 16-Bit-Variablen durch 8-Bit-Variablen ersetzt: In diesem Fall erzeugt avr-gcc ca. 4500 Bytes vs. 5100 Bytes beim Bytecode. Wenn also ausschließlich 8 Bit-Arithmetik verwendet wird, ist hier avr-gcc etwas besser als der Bytecode. Es kann jedoch auch sein, dass durch die Reduzierung auf 8 Bit viele sinnlose Anweisungen entstehen, z.B. if-Anweisungen, die immer oder nie erfüllt sind, und dass diese von avr-gcc herausoptimiert werden. Dies kann mein Compiler nicht. Diese Größenverhältnisse müssen wegen des entstehenden sinnlosen Programms natürlich nicht unbedingt repräsentativ für andere Programme sein, die mit 8- oder 16-Bit Daten arbeiten.
Florian K. schrieb: > Johann L. schrieb: >> Allerdings würde ich schätzen, ein agressiv optimierender Compiler auf >> Register-Basis schneidet besser ab als ein nicht-optimierender auf >> Stapel-Basis. > Ein paar einfache Optimierungen führt auch mein Compiler durch. Sicher > macht da GCC viel mehr. Aber ein nicht unerheblicher Teil der > Optimierungen von GCC wird wohl der Ausnutzung der Register dienen, was > bei einer Stapel-Maschine entfällt. GCC geht zunächst von einer unbegrenzten Anzahl an Registern aus. Auf diesen wird optimiert und man kann beliebig neue Register erzeugen und verwenden. Recht spät in der Compilierung (etwa Pass 180 von über 200 Passes) wird diese unbegrenzte Anzahl an Registern auf in der Maschine wiklich vorhandene Register abgebildet. Falls die realen Register nicht ausreichen, werden Werte auf den Stack ausgelagert. Nach der Register-Allokierung ist der Code kaum noch formbar, und es llaufen nur noch vergleichsweise wenige Optimierungs-Passes darauf. > Zusätzlich kann mein Compiler > bestimmte Optimierungen machen, die GCC vom Prinzip her nicht kann: > Code-Makros + Huffman-ähnliche Codierung der oft benötigten Opcodes mit > kleineren Bit-Längen. Im Compiler geht sowas natürlich nicht. Einen für einen Compiler gut verdaulichen Instruktionssatz zu entwerfen ist die Hausaufgabe des Hardwareherstellers. Um das tun zu können, braucht er schon einen Compiler (bzw. einen Compilerbauer), der Hinweise darauf gibt, welche Befehle sich lohnen (könnten) und welche eher esotherisch sind und kaum Anwendung finden würden. Das hängt natürlich von dem geplanten Einsatzfeld des Kerns ab, ist aber eine ganz interessante Aufgabe. Für aufwändige Routinen kann ein Compiler Aufrufe zu Bibliotheksfunktionen erzeugen anstatt immer wieder den Code hinzuschreiben. Das dauert zwar länger, spart aber Programmspeicher. Beispiel dafür sind etwa die Division/Modulo-Routinen, die avr-gcc von Hause aus mitbringt. > Und wie erwähnt, schneidet mein Compiler bei 32- > oder 64-Bit-Datentypen besser ab, weil die Operationen nicht aus vielen > 8-Bit Operationen zusammengesetzt werden müssen. Klar, hier bietet ne VM/Interpreter mit einem an das Problem angepassten Instruktionssatz deutliche Vorteile. Wo avr-gcc helfen könnte wäre eine bessere Unterstützung von 64-Bit Integers. Das würde zwar nicht den Bytecode verkleinern, aber den Interpreter merklich kleiner machen und auch schneller (ausser natürlich man schreibt Interpreter in (Inline) Assembler oder hat nix mit 64-Bit am Hut). Dito gilt für Beitrag "64 Bit float Emulator in C, IEEE754 compatibel" Ob eine solche Unterstützung in avr-gcc wirklich praktikabel ist, sieht man leider erst nach einer Implementierung in avr-gcc... Ich hatte mal angedacht die 64-Bit-Unterstützung hinzuzufügen, aber nach einigem Nachdenken darüber (und aus organisatorischen Gründen) die Idee wieder auf Eis gelegt: Zwei Zahlen zu addieren würde schon 16 Register belegen, und die 64-Bit Brummer hin- und herzubewegen wäre nicht trivial und das komplizierteste daran. Sinnvoll wäre womöglich die Unterstützung von Operationen direkt auf dem Speicher, denn das würde die Registerlast deutlich erniedrigen. Allerding würde das den Aufwand noch erhöhen, und eine solche Erweiterung muss auch die pathologischen Testfälle der Testsuites überstehen. > Aber wenn ich eine Register-Maschine implementieren würde, könnte ich > manche Optimierungen ebenfalls nicht so gut wie GCC durchführen, aber > die anderen weiterhin. Ich gehe davon aus, dass sich eine > Register-Maschine nur bei einem GCC-Port lohnt (wenn fertige Teile von > GCC die Optimierungen schon durchführen), nicht jedoch bei einem selbst > geschriebenen Compiler. Bei einem selbst geschriebenen Compiler ist eine Registermaschine Horror. Zumindest dann, wenn man die Register-Allokierung gut machen will: Möglichst viele Register verwenden, möglichst wenig Werte aufm Stapel zwischenspeichern müssen, und das ganze wird erschwert dadurch, daß jeder reale Prozessor über unterschiedlichste Arten von Registern verfügt ... auf dem kleinen AVR gibt's schon mindestens 7 davon! Man kann allerdings auch den BASCOM-Ansazu wählen und keine Registerallokierung machen, also immer: Wert(e) Laden, Operation, Scheiben. Vielleicht ist das sogar ne Stapelmaschine; so gut kenn ich BASCOM nicht. >> Was ist das eigentich für eine Software, für der der Interpreter >> geschrieben wurde? > Ein Text-basiertes Spiel ("Finanzspiel") für 2 oder mehr Personen, das > ich mal auf dem C64 in BASIC geschrieben habe. hehe, da kommt mit gleich VICE in den Sinn. Echt Klasse das Teil! Unbedingt antesten den C64-Simulator, wenns den noch nicht kennst! Volle Simulation, inclusive SID und VIC, es gibt ne 1541 und zahlreiche Klassiker auf (virtueller) Diskette, die man laden und ausführen kann! http://www.viceteam.org/ http://c64games.de/phpseiten/emulatoren.php > Sicher, dieses Programm ist nicht unbedingt der einzige Grund für mich, > einen Bytecode-Interpreter zu schreiben. Vielleicht könnte ich das Spiel > auch als normalen C-Code in 64K unterbringen, wenn ich mehr 8- oder > 16-Bit Datentypen verwenden würde. Aber mir ging es auch darum, mal > etwas prinzipielle Erfahrung mit der Programmierung eines > Bytecode-Interpreters zu sammeln. Das schwierige ist dabei der Compiler. Ich hab auch schon mal einen simplem Interpreter gebastelt, der in Java geschrieben war (über Parser-Generator cup und Lexer jflex), der C-ähnlichen Code ausführte, den man ins Applet-Tag schreiben konnte. > Der Bytecode-Interpreter braucht ca. > 15000 Bytes - je nach Konfiguration mehr oder weniger. Wenn man z.B. die > avr-gcc-floats benutzen will, muss man die float(32 Bit) Bibliothek > hinzurechnen. Meine float64-Bibliothek (komplett mit allen Funktionen) > braucht als Bytecode etwa 5700 Bytes, während sie als compilierter > normaler C-Code etwa 26000 Bytes braucht. Die Größeeinsparung wird bei > anderen Programmen, die mehr mit 8- oder 16-Bit-Datentypen rechnen, > wahrscheinlich deutlich geringer sein. Wie gesagt: wenn du dir mal anschaust, wie avr-gcc zwei 64-Bit Werte addiert... da wird einem echt übel :o/ Johann
Wäre nicht eine Alternative zu einer GCC-Portierung, dass man LCC verwendet: http://www.cs.princeton.edu/software/lcc/ Es handelt sich um einen ANSI C-Compiler, der irgendeinen Zwischencode erzeugt (der wahrscheinlich ähnlich wie ein Bytecode ist), aus dem man sich dann selbst einen für eine Maschine passenden Code erzeugen kann. Es werden zwar Register unterstützt, aber ich gehe davon aus, dass man genau so Code für eine Stack-Maschine erzeugen kann. Vielleicht wäre die Einarbeitungszeit in LCC geringer als wenn man GCC portieren will ? Vice habe ich noch nicht ausprobiert, aber CCS64. Vor längerem (d.h. zur Zeit von Intel 486 Prozessoren) hatte ich mein Spiel mal auf einem C64 Emulator ausgeführt (dessen Name ich nicht mehr weiß). In dem Programm werden Zufallszahlen erzeugt, indem durch den SID-Soundchip "weißes" Rauschen bei ausgeschaltetem Ton erzeugt wird und jeweils ein aktuelles Sample gelesen wird. Das konnte der Emulator außerst schlecht, d.h. die vom Emulator erzeugten Zufallszahlen waren ziemlich schlecht. Ein anderes Projekt, dass ich nun vielleicht machen werde, ist ein Jump and Run Spiel mit einem LMG6401PLG Display und einem ATMega644. Ich habe solch ein Spiel schon mal unter MS DOS programmiert (siehe google: gamble94). Wenn man eine Landschaft aus vielen "Zeichen" (z.B. 8x8 Blöcke) aufbaut, kann man mit begrenztem Speicher relativ große Spielflächen erzeugen. Mit dem LMG6401PLG Display kann man vertikal pixelweise scrollen, aber horizontal leider nur zeichenweise.
Florian K. schrieb: > Wäre nicht eine Alternative zu einer GCC-Portierung, dass man LCC > verwendet: > http://www.cs.princeton.edu/software/lcc/ > Es handelt sich um einen ANSI C-Compiler, der irgendeinen Zwischencode > erzeugt (der wahrscheinlich ähnlich wie ein Bytecode ist), aus dem man > sich dann selbst einen für eine Maschine passenden Code erzeugen kann. > Es werden zwar Register unterstützt, aber ich gehe davon aus, dass man > genau so Code für eine Stack-Maschine erzeugen kann. Vielleicht wäre die > Einarbeitungszeit in LCC geringer als wenn man GCC portieren will ? Dazu kann ich nichts sagen, den LCC hab ich noch nicht portiert; auch keine anderen retargetable compiler ausser GCC. > Vice habe ich noch nicht ausprobiert, aber CCS64. Vor längerem (d.h. zur > Zeit von Intel 486 Prozessoren) hatte ich mein Spiel mal auf einem C64 > Emulator ausgeführt (dessen Name ich nicht mehr weiß). In dem Programm > werden Zufallszahlen erzeugt, indem durch den SID-Soundchip "weißes" > Rauschen bei ausgeschaltetem Ton erzeugt wird und jeweils ein aktuelles > Sample gelesen wird. Das konnte der Emulator außerst schlecht, d.h. die > vom Emulator erzeugten Zufallszahlen waren ziemlich schlecht. VICE find ich klasse, es ist haargenau das C64 guck-und-fühl :-) > Ein anderes Projekt, dass ich nun vielleicht machen werde, ist ein Jump > and Run Spiel mit einem LMG6401PLG Display und einem ATMega644. Ich habe > solch ein Spiel schon mal unter MS DOS programmiert (siehe google: > gamble94). Wenn man eine Landschaft aus vielen "Zeichen" (z.B. 8x8 > Blöcke) aufbaut, kann man mit begrenztem Speicher relativ große > Spielflächen erzeugen. Mit dem LMG6401PLG Display kann man vertikal > pixelweise scrollen, aber horizontal leider nur zeichenweise. Da geht's dann eher um Geschwindigkeit, schätz ich. Wobei Codegröße ist immer ein Thema auf AVR, denn unnötige Instruktionen vertrödeln eben Zeit. Was mit schleierhaft ist, wie man auf einem ATmega168 sowas implementieren kann: http://www.youtube.com/watch?v=-mMdc-rNNtU Da steht was von 5 Cycles/Pixel, was heisst das überhaupt? Kann der AVR neben dem Game auch das PAL-Signal selbst erzeugen? Johann
> Da geht's dann eher um Geschwindigkeit, schätz ich. Wobei Codegröße ist > immer ein Thema auf AVR, denn unnötige Instruktionen vertrödeln eben > Zeit. Sicher, bei solch einem Spiel ist ein Bytecode-Interpreter absolut fehl am Platz. Vielleicht muss ich dazu sogar einige Low-Level-Grafikfunktionen in Assembler schreiben, obwohl ich mich dazu erst einarbeiten muss. > Wie gesagt: wenn du dir mal anschaust, wie avr-gcc zwei 64-Bit Werte > addiert... da wird einem echt übel :o/ Ich kenne zwar kein Avr-Assembler, da ich aber Assembelr von anderen Prozessoren kenne, kann ich schon sehen, dass avr-gcc recht umständlichen Code z.B. für 64-Bit-Additionen erzeugt. Wenn man dies verbessern würde (eventuell auch mit kleinen Hilfsfunktionen, um Speicher zu sparen), würde sich ein Bytecode-Interpreter eventuell sogar erübrigen, weil dies den Speicher wohl deutlich reduzieren würde. Und es hätte den Vorteil, dass es deutlich schneller als ein Bytecode-Interpreter wäre.
Zwei "C-Interpreter" 1. a) Gforth uses GCC to compile a fast threaded Forth. http://www.jwdt.com/~paysan/gforth.html (GForth ist ein in C geschriebenes Forth, so dass es sich auch als C-Interpreter miss/ge-brauchen lässt.) b) Forum: GCC -- Fragen zu ...., AVR-GCC, 2. Embedded Ch allows you to embed or plugin Ch, a powerful script engine, into your C/C++ application programs and hardware. Your C/C++ binary applications can call back into the Ch programs, Ch script functions, or access Ch global variables, etc. http://www.softintegration.com/solution/embedded/ (Ch ist ein C-Interpreter und embedded Ch eben ein in C einbedbarer C-Interpreter) Tipp: Zum privaten gebrauch ist die Studenten-Edition von Ch kostenlos, nicht aber die ebedded-edition.
Ich habe mal Ch heruntergeladen. Es scheint gar kein Problem zu sein, ein C Programm auf dem PC mit Ch interpretieren zu lassen, aber geht das auch auf einem ATMega ? Auf die Schnelle habe ich nirgendwo Hinweise darauf gefunden. Und falls der Interpreter auch auf einem ATMega läuft, wäre die Frage, wie viel Speicherplatz er braucht und wie schnell die Programme laufen. Ich könnte mir vorstellen, dass Ch auf größeren Controllern wie z.B. Cortex M3 oder STM32 läuft, aber bei diesen Controllern hat man sowieso mehr Speicher, und da wäre der Einsatz eines Interpreters allein aus Speichergründen in vielen Fällen wohl nicht gerechtfertigt. Hast Du selbst Erfahrung mit Ch bzw. Gforth ?
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.