Autor Thema: Eigene CPU  (Gelesen 68112 mal)

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« am: 17. March 2010, 23:57 »
Hallo,

vor langer Zeit gab es ja mal den Thread "Verrückter als wir...?", den ich hiermit neu erstehen lassen möchte.

Wer baut seine eigene CPU und warum? Und wie? Welche Designziele sind vorgegeben?

Da erik.vikinger ein paar Andeutungen gemacht hat, sind wir schonmal zwei mit solchen Plänen.

Für meine Architektur (ich werde die CPU wohl nicht vom Gesamtsystem trennen) habe ich mir folgende Designziele überlegt:
- in TTL realisierbar (das ist das Endziel)
- nahezu 1 MIPS/MHz (klassisch RISC, macht die Implementation einfacher, da ich die Befehle einzeln implementieren möchte)
- Befehlssatz ähnlich MIPS, aber mit MAC-Befehl (Multiply & Accumulate)
- mindestens 16 MB RAM möglich (weniger ist doof)
- wahrscheinlich 12/24 Bit Busbreite
- keine EPROMs zur ALU-Realisierung

Warum? Ich möchte gerne mein eigenes System zum Anfassen haben. Hat den Vorteil, dass man immer sagen kann "ich könnte, wenn ich wollte... da ist ein Beispiel" :-P Außerdem lernt man so viel mehr über Schaltungsentwicklung und Rechnerarchitekturen, als man vorher wollte.

Wie? Naja, derzeit entwickle ich halt ein bisschen den Befehlssatz, immer mal wieder, und habe so quasi-diffuse Vorstellungen, wie man gewisse Teile implementieren kann. Bevor ich aber Platinen ätze, möchte ich das ganze erst noch simulieren (möglichst in einem FPGA), da müsste ich mich aber auch erst einarbeiten. Zur RAM-Adressierung schwebt mir zZt eine Art Banking-System vor, quasi Segmentierung in Hardware.

Als Betriebssystem würde ich mich bevorzugt nach POSIX richten, als Compiler-Backend eventuell einen LLVM umsetzen. Da habe ich gehört, dass es recht einfach sein soll. Ist aber alles noch Zukunftsmusik.

Meldet euch zu Wort!

Gruß,
Svenska
« Letzte Änderung: 18. March 2010, 00:06 von Svenska »

Cool-Andy

  • Gast
Gespeichert
« Antwort #1 am: 18. March 2010, 06:05 »
http://mycpu.eu/
Der hat sowas selber gemacht auch mit TTL-Bausteinen. Ist aber glaube ich ein CISC.

MNemo

  • Beiträge: 547
    • Profil anzeigen
Gespeichert
« Antwort #2 am: 18. March 2010, 18:47 »
http://mycpu.eu/
Der hat sowas selber gemacht auch mit TTL-Bausteinen. Ist aber glaube ich ein CISC.

Jo. AFAIK war das auch der Auslöser für den erwähnten Thread "Verrückter als wir…?"
„Wichtig ist nicht, besser zu sein als alle anderen. Wichtig ist, besser zu sein als du gestern warst!“

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #3 am: 18. March 2010, 19:18 »
Ich wollte den Link und http://www.homebrewcpu.com/ noch anfügen, habs dann aber vergessen. Letzteres finde ich noch heftiger, aber die CPU ist wesentlich komplexer aufgebaut. Ebenfalls in TTL aufgebaut. myCPU nutzt definitiv EPROMs für die ALU, bei Magic-1 weiß ich es spontan nicht, glaube aber ja.

Gruß,
Svenska

chris12

  • Beiträge: 134
    • Profil anzeigen
Gespeichert
« Antwort #4 am: 18. March 2010, 19:19 »
ich hab auch meine eigene cpu, sogar 4 ums genau zu nerhmen ;)
ein einhalb in logisim und diese dann vollständig in c# umgesetzt als emulatoren, ich weiß sind keine echten hardware cpu's, aber ich find das schon lustig
für die letztere (xcpu) screibe ich btw grade an einem assembler und an einem compiler (xasm & xcomp) ^^
zur frage warum kann ich nichts sagen, das war, glaub ich, genauso wie bei meinem os, einfach aus spaß ...
OS? Pah! Zuerst die CPU, dann die Plattform und _dann_ das OS!

Cool-Andy

  • Gast
Gespeichert
« Antwort #5 am: 18. March 2010, 21:50 »
http://mycpu.eu/
Der hat sowas selber gemacht auch mit TTL-Bausteinen. Ist aber glaube ich ein CISC.

Jo. AFAIK war das auch der Auslöser für den erwähnten Thread "Verrückter als wir…?"

Sorry, den Thread habe ich nicht gelesen! mycpu.eu war nur das was mir Tante Google nahe legte aufzurufen.  :wink:

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #6 am: 19. March 2010, 16:02 »
Hallo,


Meldet euch zu Wort!
okay, dann melde ich mich auch mal (endlich) zu Wort:

Einige haben es ja schon mitbekommen das ich ebenfalls eine CPU entwickle. Dabei verfolge ich allerdings ein paar Ziele die nicht so ganz im Rahmen des üblichen liegen.
  • Die CPU soll eine richtige 32 Bit oder 64 Bit CPU werden, also entsprechend breite Register und Adressen (Offsets) haben, was natürlich auch in einem passend breitem Adressbus resultiert. Von der Architektur her soll es eine RISC CPU mit richtiger "Load-Modify-Store"-Philosophie und 3 Operanden-Befehlen werden. Aber ich möchte von der klassischen Pipeline etwas abweichen, die Exection-Stage und die Memory-Access-Stage sollen parallel arbeiten. Rechenbefehle können also keine Speicherzugriffe durchführen und Speicherzugriffsbefehle keine Rechenoperationen, mit Ausnahme von Adressberechnungen welche aber auch etwas anders gerechnet werden sollen (die Memory-Access-Stage hat also ein eigenes speziell passendes Rechenwerk). In der Adress-Berechnung will ich vor allem keinen Überlauf haben, wenn also z.B. auf 0x00020000 + 0xFFFFFFF0 zugegriffen wird dann kommt nicht 0x0001FFF0 raus sondern eine Exception, damit fallen ein paar blöde Fallstricke weg. Außerdem soll es um das Exection/Memory-Access Duo eine Umgehung geben so das MOV-Befehle eine Abkürzung haben.
    Falls ich es hin bekomme will ich Out-of-Order-Execution realisieren, aber das ist dann bereits die Oberliga.
  • Als herausragendstes Merkmal für meine CPU möchte ich richtige Segmentierung unterstützen, ähnlich der des 386 im PM. Jedes Segment hat eine individuelle Basis (Page-Alignment) ein Limit und ein Minimum (beides mit mindestens 256 Byte Alignment). Das Minimum dient dazu das z.B. ein NULL-Pointer auffällt, kann aber auch dazu verwendet werden das vorne in einem Segment nicht nutzbarer Platz bleibt der aber nicht im linearen Speicher verloren geht (z.B. für ALR). Die Selectoren sind 16 Bit groß und erlauben den Zugriff auf 2 verschiedene Descriptor-Tabellen, GDT (gibts nur ein mal im System) und LDT (jeder Prozess hat seine eigene). Beide enthalten bis zu 32768 Segmente und jeder Descriptor ist 16 Bytes groß.
    Alle Segmente von allen Prozessen teilen sich den einen linearen Speicher, der auf der 32 Bit Variante "nur" 4 GByte groß ist aber auf der 64 Bit Variante wohl (zumindest vorerst) keine Enge darstellt.
    Dieser lineare Speicher soll zur Laufzeit defragmentiert werden, falls es eng wird und viele kleine Lücken da sind. Mit Hilfe vom Paging ist das auch keine Hexerei und funktioniert völlig unbemerkt im Hintergrund.
    Das Paging ist über alle CPUs im System synchron, es gibt also nur ein einziges Paging-Directory für alles, Speicherschutz kommt ja von der Segmentierung. Das Paging soll nur dann angeschaltet werden wenn es tatsächlich benötigt wird, also für Speichermagie wie Defragmentierung oder eben zum swappen auf einen Massenspeicher.
    Um z.B. Speicherbereiche der Hardware effizient nutzen zu können soll im Segmentdescriptor ein Bit drin sein das bestimmt ob für Zugriffe über dieses Segment überhaupt das Paging benutzt werden soll. So kann das Paging-Directory möglichst klein bleiben und kostet dort keine Performance. Außerdem ist es so möglich dass das OS trotz eingeschaltetem Paging flach auf den physischen Speicher zugreifen kann.
  • Normale Register will ich 64 haben, wobei die Register R60 bis R63 spezielle Aufgaben (Flags, Link-Register, Stack-Pointer und Instruction-Pointer) erledigen. Ansonsten können alle Register von allen Befehlen für alles benutzt werden. Dazu kommen noch 16 Segmentregister, wobei auch hier S12 bis S15 spezielle Aufgaben (Data-Segment, Const-Segment, Stack-Segment und Code-Segment) wahrnehmen.
    Zur Konfiguration will ich mehrere Configuration-Register-Spaces haben welche immer 64 k-Register fassen (sowas ähnliches wie die MSR auf x86). Jeder Configuration-Register-Space hat bestimmte Aufgaben. Es gibt einen lokalen Konfigurations-Register-Satz der für eine bestimmte CPU gilt (da sind Dinge wie Zugriffsrechte oder die LDT-Adresse vom aktuellen Thread drin), einen globalen Konfigurations-Register-Satz (damit wird das Paging, die GDT und die MTRRs für alle CPUs verwaltet), einen Konfigurations-Register-Satz für den System-Controller (eine Art North-Bridge, da sind IRQ-Controller, Uhr usw. drin) und der 4te Konfigurations-Register-Satz ist eher virtuell (darüber kann eine CPU direkt auf den lokalen Configuration-Register-Space einer bestimmten anderen CPU zugreifen).
  • Der Befehlssatz wird eine Mischung aus ARM, AVR32 und Itanium. Die Befehle werden in 128 Bit-Paketen verpackt. Jedes Paket kann 2 60 Bit Befehle, 3 39 Bit-Befehle, 6 20 Bit-Befehle oder beliebige Kombinationen davon enthalten. In jedem Befehls-Paket sind die ersten 8 Bit eine Art Header der bestimmt was in den anderen 120 Bit drin ist. Die 20 Bit Befehle sind nur kleine 2 Operanden-Versionen der großen Befehle, ein paar dieser Befehle können nur die ersten 32 Register benutzen so das nur 5 Bit pro Operand benötigt werden. Bei den 20 Bit Befehlen können nur die Sprünge bedingt ausgeführt werden, alle anderen Befehle sind immer unbedingt. Die 60 Bit Befehle sind immer bedingt ausführbar, wofür immer 7 Bit benötigt werden so das effektiv "nur" 53 Bit als Befehl zur Verfügung stehen. Bei den 39 Bit Befehlen gibt es einen Zusatztrick, wenn die mit anderen Befehlslängen in einem Paket sind unterstützen sie nur bedingte Sprünge (alles andere ist dann immer unbedingt), wenn aber nur 39 Bit Befehle in einem Paket drin sind dann ist der Paket-Header 11 Bit groß und es kann eine beliebige Kombination der 3 Befehle bedingt ausgeführt werden. Mit dieser Flexibilität bei der bedingten Ausführung lassen sich z.B. kleine if-else-Konstrukte komplett ohne Sprünge realisieren.
    Das laden von 64 Bit Konstanten ist mit 2 60 Bit Befehlen möglich (PowerPC braucht dazu 5 32 Bit Befehle und mehr Takte, Itanium braucht nur 2 von 3 Slots in seinen 128 Bit Paketen dafür müssen beide Slots hintereinander im selben Paket sein), falls nicht alle 64 Bits mit nichtzusammenhängenden Bits belegt sind können auch kleinere Kombinationen benutzt werden (diese Möglichkeit hat PowerPC auch aber Itanium nicht). 32 Bit Konstanten können (unter gewissen Voraussetzungen) mit einem 39 Bit Befehl geladen werden.
    Das OpCode-Format ist insgesamt recht komplex und der Decoder wird auch sicherlich eine der größten Komponenten der CPU werden, aber dafür bekomme ich recht kompakten Code der trotzdem möglichst Leistungsfähig ist. Durch denn schlankeren Code ist die erforderliche Speicherbandbreite kleiner bei trotzdem hoher Performance. Wer sich die Vorteile und Nachteile der ARM- und Thumb-Modi anschaut versteht was ich meine, bei ARM muss man sich aber auf Funktionsebene entscheiden ob man 32 Bit oder 16 Bit Befehle verwendet, diesen Nachteil möchte ich nicht. Auf dem AVR32 gibt es auch viele der 32Bit Befehle, mit eingeschräkter Flexibilität, als 16 Bit Befehl.
    Vom Vorbild ARM habe ich noch die Shifter-Operands übernommen, ein nettes und nützlichen Feature. Von AVR32 sind einige der Speicher-Adressierungsmodi.
    Das explizit parallele ausführen aller Befehle in einem Paket, so wie beim Itanium, will ich nicht. Aus Sicht des Programmierers werden alle Befehle seriell ausgeführt, solange diese Sicht gewahrt bleibt darf die CPU natürlich umsortieren wie sie will.
    Auch lässt sich jeder Befehl in einem Paket individell anspringen, es müssen also keine NOPs zum auffüllen von Paketen benutzt werden. Ebenso gibt es keine Beschränkungen bezüglich der kombinierbaren Befehlstypen (ALU, Branch oder Memory) so das man keine NOPs braucht um nicht nutzbare Befehls-Slots zu füllen. Beim Itanium ist beides nicht so, weswegen dessen Code wegen vieler NOPs oft sehr viel größer ist als auf anderen CPUs (was Intel mit riesigen Caches versucht auszugleichen).
  • Ein weiteres Alleinstellungsmerkmal sind die Flags, in R60. Das die Flags in einem normalem Register liegen hat den Vorteil das sie dort einfach zu sichern und wiederherzustellen sind ohne spezielle Befehle zu benötigen. Außerdem sind es 3 oder 4 komplette und unabhängige Flag-Sets. Damit werden Abhängigkeiten zwischen den Befehlen vermieden, Alpha-CPUs haben deswegen gar keine Flags sondern prüfen in bedingten Befehlen immer direkt ein beliebiges Register ob sein Inhalt einem bestimmten Kriterium entspricht. Mit den unabhängigen Flags kann man z.B. mehrere Befehle hintereinander bedingt ausführen ohne dieses Flag-Set zu zerstören weil die Befehle für ihre eigentliche Aufgabe ein anderes Flag-Set benutzen können. Auch müssen der Befehl der eine bestimmte Prüfung vornimmt und damit Flags setzt und der zugehörige bedingte Befehl nicht hintereinander stehen, alle dazwischenliegenden Befehle können andere Flag-Sets nutzen. Dafür verzichte ich auf die Möglichkeit das die Befehle bestimmen können ob sie die Flags modifizieren, es wird auch keinen CMP-Befehl oder TEST-Befehl geben.
    Auch die Gleitkomma-Flags (inexact, overflow, underflow usw.) sollen in die User-Mode-Flag-Sets rein. System-Flags (wie Interrupt-Enable u.ä.) gehören IMHO nicht in die User-Flags und werden in den lokalen Controll-Registern landen. Das ein einfacher Flag-Lese/Schreib-Befehl potentiell gefährlich ist und die CPU da aufpassen muss das der User-Mode-Code keine unerlaubten Dinge tut finde ich doof (so ist es leider bei vielen mir bekannten CPU-Architekturen und nicht nur bei x86).
  • SMP ist auch ein wichtiges Design-Ziel. Daher lege ich auch großen Wert auf zuverlässige atomare Befehle. Code-Cache möchte ich gleich im ersten FPGA-Anlauf einbauen, der Daten-Cache wird wohl erst in der zweiten Version kommen. Cache-Kohärenz-Protokolle sind jedenfalls keine triviale Angelegenheit. Die meisten Speicherzugriffsbefehle werden jedenfalls Flags haben mit dehnen bestimmt werden kann ob die Daten überhaupt in den Cache sollen.
    Für die Atomizität von Befehlen und Befehlssequenzen möchte ich die Konzepte von ARM übernehmen. Die gefallen mir sehr gut, bieten viel Flexibilität, scheinen mit vertretbarem Aufwand umsetzbar zu sein und machen einen sehr zuverlässigen und sicheren Eindruck.
    Jede CPU soll einen eigenen Timer fürs preemptive Multitasking bekommen. Dieser Timer hat nur 24 Bit und zählt mit 16 MHz nach 0, bei 0 gibt es dann eine passende Kontext-Switch-Exception. Dieser Timer wird nur zählen wenn die CPU sich im User-Mode befindet. Ich hab zusätzlich noch einen speziellen Befehl vorgesehen mit dem die Software gezielt eine Kontext-Switch-Exception auslösen kann.
  • Interrupts sind ein weiterer wichtiger Punkt in meiner Plattform. Im System-Controller, eine Art North-Bridge welche die Verbindung zwischen der/den CPU(s) und der Peripherie herstellt (also einen oder mehrere PCI-Roots enthällt), soll ein IRQ-Controller sein der die IRQs der Peripherie per MSI(-X) entgegen nimmt. Für die Peripherie-IRQs gibt es einen generischen IRQ-Handler (dessen Adresse in einem Controll-Register im System-Controller steht) im OS der die IRQ-Nummer als Parameter bekommt und dann eine passende IPC-Message an den Treiber schickt. Zusätzlich gibt es noch ein paar feste IRQs die eigene Handler haben, z.B. für die HW-Uhr.
    Bei der IRQ-Verteilung will ich das die CPUs sich untereinander verständigen welche gerade den User-Mode-Code mit der niedrigsten Priorität ausführt und diese den IRQ entgegen nimmt (also aktiv beim System-Controller abholt), dazu hab ich mir eine simple Analogschaltung ausgedacht bei welcher der aktuellen Priorität ein Spannungswert auf einer gemeinsamen Leitung zugeordnet wird und alle CPUs sehen ob eine andere CPU eine niedrigere Priorität hat und dann das IRQ-Signal ignorieren. IRQs werden nur im User-Mode entgegen genommen und es wird auch keine Möglichkeit geben dass das abgeschaltet werden kann. Es soll aber möglich sein das der User-Mode-Code die IRQ-Annahme gezielt für ein paar Befehle abstellen kann damit atomische Befehlssequenzen nicht unnötig unterbrochen werden.
  • Beim Paging hab ich noch nicht alle Details zu Ende gedacht. Für die 32 Bit Version meiner CPU möchte ich das Paging vom 586 mit Pages von 4 kByte und 4 MByte übernehmen, nur das die ganzen Flags in den Page-Descriptoren fehlen. Für die 64 Bit Variante bin ich noch etwas unentschlossen. Ich möchte auf jeden Fall den gesamten 64 Bit linearen Adressraum nutzen können, nicht so wie bei x86_64 wo der virtuelle Adressraum nur 48 Bit groß ist. Bei 64 Bit möchte ich gerne guarded Page-Tables nutzen um Tabellen-Ebenen überspringen zu können.
    Auf jeden Fall möchte ich einen Mechanismus implementieren mit dem ein bestimmter Page-Descriptor aus allen TLBs gelöscht werden kann (mit einer Art Broadcast-Message an alle CPUs) ohne das die CPUs irgendwie beim arbeiten gestört werden müssten (das ist für die Hintergrund-Speicher-Defragmentierung sehr wichtig).
Die Idee zu dieser CPU/Plattform-Architektur ist etwa 2007 entstanden. Ich hab bestimmt 2 Jahre nur über dem Befehlssatz und der Plattform-Architektur gebrütet bis ich mich vor gut einem Jahr dazu entschlossen habe einen Simulator zu programmieren. Der Simulator kann auch schon einiges, aber noch längst nicht alles. Da der Befehlssatz jetzt einigermaßen fest ist, ich sehe zumindest momentan keine Notwendigkeit mehr noch mal größere Änderungen vornehmen zu müssen, hab ich mich entschlossen als nächstes einen Assembler zu programmieren (das ist meine aktuelle Tätigkeit an diesem Projekt) damit ich den Code für den Simulator nicht immer per Hex-Code eingeben muss (was dank der krummen Bit-Offsets in den Befehls-Paketen auch echt bescheiden ist). Sobald der Assembler ordentlich funktioniert werde ich auch wieder am Simulator weiter arbeiten. Sobald der Simulator einigermaßen funktioniert will ich dann am OS anfangen, wie weit ich das ohne einen C/C++-Compiler treiben möchte weiß ich noch nicht. Wenn das alles ordentlich läuft will ich meine Plattform in mehreren FPGAs implementieren. Dafür existieren auch bereits verschiedene VHDL-Bruchstücke, von dehnen ein paar zumindest in der Simulation schon ganz gut arbeiten.

Als OS möchte ich einen schlanken Micro-Kernel implementieren der außer Speicherverwaltung, Prozess/Thread-Management, Scheduling, Timing und IPC kaum etwas enthält. Das IPC-Konzept hab ich ja schon mal vorgestellt.



- in TTL realisierbar (das ist das Endziel)
Das dürfte teuer werden. Ordentliche FPGA-Starter-Kits, speziel für Soft-CPU-Entwicklung, gibt es bereits ab gut 250 Euronen und da ist dann ordentlich DRAM und Flash zusammen mit ein paar Schnittstellen (manchmal sogar Ethernet) drauf.

- Befehlssatz ähnlich MIPS, aber mit MAC-Befehl
Also richtige 3 Operanden Befehle möchte ich nicht mit TTL realisieren müssen. Da solltest Du Dich eher an CISC halten und versuchen komplexe Befehle seriell einzulesen und zu verarbeiten. Aber wenn Du wirklich RISC möchtest dann nur zu, ist eben eine Herausforderung.

- keine EPROMs zur ALU-Realisierung
Das ist zwar löblich, macht es Dir aber nur noch schwieriger.

Ich möchte gerne mein eigenes System zum Anfassen haben. .... Außerdem lernt man so viel mehr über Schaltungsentwicklung und Rechnerarchitekturen, als man vorher wollte.
Zwei sehr gute Gründe. Sind bei mir die selben. :-D

Wie? Naja, derzeit entwickle ich halt ein bisschen den Befehlssatz, immer mal wieder, und habe so quasi-diffuse Vorstellungen, wie man gewisse Teile implementieren kann.
Das wirst Du dann aber mal konkretisieren müssen. Versuche einfach mal ein paar kleine Algorithmen händisch in Assembler-Code für Deine CPU umzusetzen um ein Gefühl dafür zu bekommen ob es noch Stellen gibt wo es möglicherweise hackt. Ich empfehle Dir die OpCodes in einer Tabelle zu verwalten, so behält man einen guten Überblick welche Bit-Kombinationen schon belegt sind und welche noch frei sind.
Auf wie vielen CPU-Architekturen kannst Du denn (fließend) Assembler? Man sollte IMHO schon etwas Erfahrung in der Benutzung mehrerer verschiedener CPU-Architekturen haben um nicht gleich zu viele Fehler zu machen. Wenn Du erst später beim programmieren von Programmen für Deine CPU merkst das der Befehlssatz ein paar Probleme hat wirst Du Dich sicher ziemlich ärgern weil Du dann entscheiden darfst ob Du noch mal von vorne anfängst oder ob Du mit dem Problem irgendwie leben willst. Das ist IMHO die ekligste Sorte von Entscheidungen die man treffen muss.

(möglichst in einem FPGA), da müsste ich mich aber auch erst einarbeiten.
Das ist aber kein leichtes Thema, betrachte es als Herausforderung.

Zur RAM-Adressierung schwebt mir zZt eine Art Banking-System vor, quasi Segmentierung in Hardware.
Wie stellst Du Dir das vor? So ähnlich wie auf einigen Mikro-Controllern? Oder eher in Richtung x86 Real-Mode? Compilerunterstützung ist damit übrigens ein ernstes Problem, (fast) alle heutigen Compiler gehen von Flat-Memory aus. Bezüglich meiner Segmentierung hab ich mich auch sehr intensiv umgeschaut und bin heute der Meinung das der LLVM da noch der interessanteste Kandidat ist (zwischenzeitlich hatte ich mal den gcc favorisiert), ich denke ich kann dem LLVM noch am ehesten meine Segmente beibringen, er arbeitet (fast) nie direkt mit Pointern so das ich diese spezielle Komplexität wohl gut verbergen kann.



ich hab auch meine eigene cpu, sogar 4 ums genau zu nerhmen ;)
ein einhalb in logisim und diese dann vollständig in c# umgesetzt als emulatoren, ich weiß sind keine echten hardware cpu's, aber ich find das schon lustig
für die letztere (xcpu) screibe ich btw grade an einem assembler und an einem compiler (xasm & xcomp)
Kannst Du dazu mal was genaueres schreiben? Ist bestimmt interessant.



Wenn jemand Fragen, Anregungen oder Kritik zu meinem Konzept hat dann nur zu.


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

Cool-Andy

  • Gast
Gespeichert
« Antwort #7 am: 19. March 2010, 16:11 »
 :-o Boa! Hast du den schon ne Website für dein Projekt?

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #8 am: 19. March 2010, 18:57 »
Hallo,

das war eine ziemlich ausführliche Beschreibung des Systems. Und ich merke den Unterschied zu meiner Idee. :-)

Ein Designziel bei mir ist, wie gesagt, die Implementierbarkeit in TTL. Dass das nicht billig wird, ist mir klar, aber dafür ist es ein Hobby. :-D Wahrscheinlich werde ich aber trotzdem eine FPGA-Lösung entwickeln, vermutlich wird ein CPLD aber nicht ausreichen. :-(

Ich möchte mein System recht einfach stricken, also mich nicht an x86 orientieren, sondern eher an kleinen 8-Bit-Systemen und die "etwas" aufbohren. Performance ist bei mir zweitrangig und wird hinter die Implementierbarkeit gestellt.

Der Befehlssatz soll nur die wirklich notwendigen Befehle bereitstellen (plus einen MAC-Befehl, den halte ich für nützlich) und auch keine 3-Operanden-Form haben; das ganze System wird sehr Akkumulator-zentrisch gebaut werden (wobei ich noch nicht weiß, ob ich ein oder zwei Akkumulatorregister haben werde). Hardwareseitig hätte ich bevorzugt einen Instruktionsbus, auf dem ich die einzelnen Instruktionen verarbeiten lasse. Um den Aufwand zu begrenzen, werde ich nur 16 (evtl. 32) verschiedene Befehle zulassen, denen dann in der Instruktion selbst noch Daten (=Registeradressen) folgen. Somit könnte ich jeden Befehl (charakterisiert durch die ersten 4/5 Bits) auf einer Platine realisieren; bei Logikbefehlen möglicherweise noch mehr.

Alle Instruktionen sollen in einem Taktzyklus abgearbeitet werden, mir schweben etwa 3-5 MHz (bei einer Gattergeschwindigkeit von ~20-50 MHz, im FPGA natürlich wesentlich mehr) vor. Das sollte realistisch machbar sein. Um die Implementation zu vereinfachen, gibt es nur zwei Befehle für den Speicherzugriff (LOAD/STORE) und nur einen Befehl für Konstanten (LOAD IMMEDIATE). Diese Befehle werden vermutlich zwei (oder drei) Takte benötigen, um weitere Datenworte einzulesen; alle anderen Befehle sollen in einem Takt und ausschließlich mit Registern abgearbeitet werden.

Die Speicheransteuerung wird ebenso simpel sein. Da ich mind. 24-Bit-Adressen (=16 MB) auf einer schmaleren Adress-/Datenbreite haben möchte, schwebt mir ein Banking-System wie aus Frühzeiten vor; ich werde also Speicherblöcke in den Adressraum einblenden. Die Blockgröße und die Anzahl der Blöcke stehen aber noch nicht fest.

Allein diese Speicheraufteilung erinnert stark an Overlay-Technik, wo mir jeder Compiler was husten wird. Mit ein bisschen Spielerei an den Adressleitungen sollte ich ein flacher Speicherraum aber simulieren lassen, wenn auch der Overhead nicht gerade leicht sein wird. Im Prinzip handelt es sich aber um eine Segmentierung mit Segmenten festgelegter Größe (und Zugriffe auf andere Segmente kosten viel Performance), daher bin ich schon an Segmentierung interessiert.

Paging, Speicherschutz und SMP sind in der Idee nicht vorgesehen.

Als Bussystem wird wohl der Speicherbus herhalten müssen, dann hätte ich Speicherbänke, die nicht mit Speicher versehen sind, sondern direkt die Hardware ansprechen. Damit sollte sich relativ einfach ein I2C- oder SPI-Interface basteln lassen, welches als I/O dient. Wenn ich den Adressraum in vier frei mapbare Bänke zerlege, lässt sich das auch recht performant ansprechen.

Das alles sind aber nur Gedanken zur Umsetzung einiger Details, ein Gesamtsystem habe ich noch nicht entworfen. Beispielsweise steigt der Lötaufwand mit steigender Busbreite stark an, aber ein zu schmaler Bus schränkt den Adressraum (und damit die sinnvolle Programmierung) stark ein. Da einen guten Kompromiss zu finden ist schwierig bis unmöglich. Es wird mir wahrscheinlich nicht gelingen, einen TCP/IP-Stack ohne Würgereize in 1K oder 4K-Overlays zu zerlegen. Andererseits sind 256K-Blöcke (18 Bit) auf einer 12-Bit-Architektur beschissen zu adressieren...

Das OS wird natürlich in diesen Beschränkungen auch leben müssen, ich strebe aber grundsätzliche POSIX-Kompatiblität an. Der Kernel selbst wird vermutlich außer Speicher- und Taskverwaltung nur eine Filesystemschnittstelle bieten, über die der Hardwarezugriff möglich wird. Allerdings sind das nur Gedanken, die ich bisher noch weg schiebe.

Ich würde auch gerne von chris12 hören wollen, wie er seine CPUs gestrickt hat!

Was sagt ihr zu dem Grobkonzept erstmal?

Gruß,
Svenska

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #9 am: 19. March 2010, 20:16 »
Hallo,


das war eine ziemlich ausführliche Beschreibung des Systems.
Das war ein sehr grober Umriss meines Konzepts. :-D Da fehlen noch etliche Dinge.

Wahrscheinlich werde ich aber trotzdem eine FPGA-Lösung entwickeln, vermutlich wird ein CPLD aber nicht ausreichen. :-(
Also für einen CPLD müsste Deine CPU schon sehr primitiv sein. Hast Du Dich schon für eine FPGA-Serie entschieden? Ich persönlich möchte auf die Spartan 6 von Xilinx setzen, aber die LXT-Variante. Zur Verkabelung der einzelnen Platinen untereinander will ich dann gekreuzte SATA-Kabel benutzen, die sind relativ billig und trotzdem gut.

Ich möchte mein System recht einfach stricken, also mich nicht an x86 orientieren,
Der Original i8086 war schon recht simpel und der i8088 hatte sogar nur einen 8 Bit-Datenbus.

sondern eher an kleinen 8-Bit-Systemen und die "etwas" aufbohren.
Eine interessante Idee, da gibt es viele Konzepte von dehnen Du was abschauen kannst.
Kennst Du die 8 Bit AVRs von Atmel? Die habe nur 8 Bit Register (und gleich 32 Stück) können aber zur Speicheradressierung 2 davon zusammenfassen also 64 kBytes adressieren. Du könntest z.B. 16 Stück von 16 Bit Registern haben und immer 8 Paare für die flache Adressierung nutzen. Das bekommst Du auch dem Compiler ganz einfach beigebracht. Lade Dir bei Atmel.com einfach mal die Core-Spec für die 8 Bitter runter und ließ Dir das durch, wenn das Konzept interessant für Dich ist und/oder Du fragen hast kannst Du Dich gerne bei mir melden.

Performance ist bei mir zweitrangig und wird hinter die Implementierbarkeit gestellt.
Also für mich ist hohe Performance schon ganz klar ein Design-Ziel. Nur die Funktionalität, welche ich verwirklichen möchte, ist mir wichtiger.

Der Befehlssatz soll nur die wirklich notwendigen Befehle bereitstellen (plus einen MAC-Befehl, den halte ich für nützlich) und auch keine 3-Operanden-Form haben; das ganze System wird sehr Akkumulator-zentrisch gebaut werden (wobei ich noch nicht weiß, ob ich ein oder zwei Akkumulatorregister haben werde).
Du hattest von MIPS geredet, daher bin ich von festen 32Bit-Befehlen mit 3 (manche Befehle haben auch 4) Operanden ausgegangen, so wie auf den meisten RISC-CPUs (Alpha, MicroBlaze, PowerPC, Sparc usw.). Erzähl doch mal was genaueres über die Befehls-Architektur. Sollen die Befehle z.B. alle die gleiche Größe haben?

Alle Instruktionen sollen in einem Taktzyklus abgearbeitet werden, mir schweben etwa 3-5 MHz (bei einer Gattergeschwindigkeit von ~20-50 MHz, im FPGA natürlich wesentlich mehr) vor. Das sollte realistisch machbar sein.
Single-Cycle sollte Dir eine Zweistufige-Pipeline ermöglichen (so wie bei den 8 Bit AVRs). Zur Frequenz kann ich nichts sagen, ich denke das ist einer der Grüde warum der Macher von mycpu.eu sich einen Gate-Level-Simulator geschrieben hat der auch die Laufzeiten der Signale und der TTL-Bauteile berücksichtigt.

Um die Implementation zu vereinfachen, gibt es nur zwei Befehle für den Speicherzugriff (LOAD/STORE) und nur einen Befehl für Konstanten (LOAD IMMEDIATE).
Das macht es auch dem Compiler recht einfach. Ich hab das ähnlich vorgesehen nur das ich noch viele Rechen-Befehle hab die auch mit Immediates umgehen können und auch komplexere Adressierungsarten für die Speicherzugriffe. Zusätzlich hab ich noch Speicherzugriffs-Befehle für mehrere Register vorgesehen (so wie LDM und STM bei ARM).

... schwebt mir ein Banking-System wie aus Frühzeiten vor; ich werde also Speicherblöcke in den Adressraum einblenden. Die Blockgröße und die Anzahl der Blöcke stehen aber noch nicht fest.
Allein diese Speicheraufteilung erinnert stark an Overlay-Technik, wo mir jeder Compiler was husten wird.
Oh ja, damit bekommst Du sicher Probleme (nicht nur mit dem Compiler). Von richtigem Banking würde ich persönlich eher abraten.

Paging, Speicherschutz und SMP sind in der Idee nicht vorgesehen.
Hatte ich bei einem TTL-Projekt auch nicht wirklich erwartet. :wink:

Es wird mir wahrscheinlich nicht gelingen, einen TCP/IP-Stack ohne Würgereize in 1K oder 4K-Overlays zu zerlegen.
Ein Grund mehr vom Banking Abstand zu nehmen. 1 oder 4 kBytes sich wirklich zu wenig 16 kBytes sollten es schon sein.

.... Allerdings sind das nur Gedanken, die ich bisher noch weg schiebe.
Ja, sowas kommt erst wenn Du Dich auf eine Befehls- und Plattform-Architektur festgelegt hast. Natürlich musst Du die gewünschten OS-Konzepte immer mit im Blick behalten.


Hast du den schon ne Website für dein Projekt?
Nein. Ich bin mir noch nicht mal beim Namen sicher. Was mir auch noch fehlt ist eine geeignete Lizenz. Bis jetzt sieht für mich nur die Qt-Lizenz interessant aus.


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #10 am: 20. March 2010, 00:25 »
Wahrscheinlich werde ich aber trotzdem eine FPGA-Lösung entwickeln, vermutlich wird ein CPLD aber nicht ausreichen. :-(
Also für einen CPLD müsste Deine CPU schon sehr primitiv sein. Hast Du Dich schon für eine FPGA-Serie entschieden? ...
Überhaupt nicht. Die Begrenzung liegt da im Geld (und in den anderen Projekten, die nebenher noch so laufen), daher hab ich mich damit noch nicht so wirklich beschäftigt.

Ich möchte mein System recht einfach stricken, also mich nicht an x86 orientieren,
Der Original i8086 war schon recht simpel und der i8088 hatte sogar nur einen 8 Bit-Datenbus.
Naja, ich werde CPU und System bei mir nicht trennen. Außerdem ist der 8088 intern relativ komplex... ich möchte den ja nirgends einbauen, sondern die CPU selbst entwickeln.

Kennst Du die 8 Bit AVRs von Atmel? Die habe nur 8 Bit Register (und gleich 32 Stück) können aber zur Speicheradressierung 2 davon zusammenfassen also 64 kBytes adressieren. Du könntest z.B. 16 Stück von 16 Bit Registern haben und immer 8 Paare für die flache Adressierung nutzen.
Die AVR kenne ich, hab ich schon programmiert. Register sind aber in TTL sehr, sehr umständlich und teuer zu implementieren, weswegen ich möglichst wenig davon haben möchte. Ich denke mit ~<10 Registern je 8 bzw. 12 Bit auskommen zu können, muss mir da aber noch mehr Gedanken machen.

Der Befehlssatz...
Du hattest von MIPS geredet, daher bin ich von festen 32Bit-Befehlen mit 3 (manche Befehle haben auch 4) Operanden ausgegangen, so wie auf den meisten RISC-CPUs (Alpha, MicroBlaze, PowerPC, Sparc usw.). Erzähl doch mal was genaueres über die Befehls-Architektur. Sollen die Befehle z.B. alle die gleiche Größe haben?
Ich dachte eher an den Befehlssatz MIPS I, aber nicht dessen encodierung. Alle Befehle sollen die gleiche Länge haben (eine Datenbreite), um in einem Takt auch geladen werden zu können. Einzige Ausnahme werden LOAD, STORE und LOAD IMMEDIATE sein müssen; wahrscheinlich werde ich dafür eine Art Zustandsautomat realisieren.

Der Aufbau ist möglicherweise:
- 4 Bit Instruktion (=Instruktionsplatine)
- 1 Bit Akkumulatorauswahl (=Ziel)
- 3 Bit Registerauswahl (=Quelle)

Wahrscheinlich werde ich aber 5 Bit Instruktion vorsehen müssen, und bei einer 12-Bit-Architektur habe ich auch wesentlich mehr Spielraum für die Codierung. Das heißt auch, es wird unbenutzte Opcodes geben, je nachdem, wieviel Arbeit ich auf eine Europlatine stecken kann.

Alle Instruktionen sollen in einem Taktzyklus abgearbeitet werden, mir schweben etwa 3-5 MHz (bei einer Gattergeschwindigkeit von ~20-50 MHz, im FPGA natürlich wesentlich mehr) vor. Das sollte realistisch machbar sein.
Single-Cycle sollte Dir eine Zweistufige-Pipeline ermöglichen (so wie bei den 8 Bit AVRs). Zur Frequenz kann ich nichts sagen, ich denke das ist einer der Grüde warum der Macher von mycpu.eu sich einen Gate-Level-Simulator geschrieben hat der auch die Laufzeiten der Signale und der TTL-Bauteile berücksichtigt.
Ich werde wohl keine Pipeline implementieren, das treibt den Aufwand in die Höhe. Höchstens eine Pipeline zwischen "Lesen" und "Ausführen", wobei ich (a) bei Sprüngen dem Compiler das NOP überlasse und (b) nichtmal einen Ahnungsansatz habe, wie ich länger andauernde Befehle in diesen Zyklus einbinden könnte. Es sei denn, ich halte die CPU und damit die Pipeline an...

Einen timingkritischen Simulator werde ich wohl nicht bauen, sondern den FPGA als Testbett nehmen; wenn ich die kritischen Pfade auszähle und gleichartige Gatter verbaue, sollte sich das einigermaßen ausrechnen lassen. Dabei ist es für mich eine Pfadlänge von 10, was mir vorschwebt. Allerdings ist das eher ne diffuse Geschichte.

... schwebt mir ein Banking-System wie aus Frühzeiten vor; ich werde also Speicherblöcke in den Adressraum einblenden. Die Blockgröße und die Anzahl der Blöcke stehen aber noch nicht fest.
Allein diese Speicheraufteilung erinnert stark an Overlay-Technik, wo mir jeder Compiler was husten wird.
Oh ja, damit bekommst Du sicher Probleme (nicht nur mit dem Compiler). Von richtigem Banking würde ich persönlich eher abraten.
Es vereinfacht die Implementation aber ungemein, da ich die gesamte "MMU" in zwei-drei Gatter verlagern kann und trotzdem die Adressleitungen einspare. Das ist der diskreten Realisierung geschuldet; ich kann keine 32 Leitungen eines Bussystems durch mehrere Platinen schaufeln und trotzdem noch Platz für Logik haben.

Wenn ich dem Compiler einen virtuellen Zeiger über mehr als den Adressraum gebe und ihm mitteile, dass er die Bank vorher extra belegen muss, dann sollte ich aber lauffähigen (und krass unperformanten) Code hinkriegen. Flatmem-Simulation eben. Vielleicht muss ich später auch einen C-Ableger basteln, wo ich dem Compiler die RAM-Verwaltung abnehme und den Teil per Hand machen muss.

Es wird mir wahrscheinlich nicht gelingen, einen TCP/IP-Stack ohne Würgereize in 1K oder 4K-Overlays zu zerlegen.
Ein Grund mehr vom Banking Abstand zu nehmen. 1 oder 4 kBytes sich wirklich zu wenig 16 kBytes sollten es schon sein.
Ich überlege, wie ich einen 1 MB Adressraum in vier 256K-Bänke zerlegen kann und das trotzdem in eine Busbreite von 12/24-Bit kriege. Ohne sinnlos Bits zu verschwenden. Zumindest die Größenordnung schwebt mir vor. Außerdem sind 64 Bänke (davon vllt. 48 nutzbar) genug, um die Organisation für das OS einfach zu halten. Jedes Programm kriegt dann eine RAM-Bank für sich, eventuell für Code/Daten/Stack je eine.

.... Allerdings sind das nur Gedanken, die ich bisher noch weg schiebe.
Ja, sowas kommt erst wenn Du Dich auf eine Befehls- und Plattform-Architektur festgelegt hast. Natürlich musst Du die gewünschten OS-Konzepte immer mit im Blick behalten.
Das erinnert mich an die IPC-Architektur von AmigaOS (Mikrokernel) - einfach einen JUMP in den Kernel. Wenn die Hardware eh keinen Speicherschutz bietet, ist das eine schnelle Variante mit den Vorteilen eines Mikrokernels. Prinzipiell orientiere ich das OS an der Hardware statt umgekehrt.

Der Befehlssatz selbst ist ja quasi-festgelegt, nur die Codierung noch nicht. Und natürlich die Architektur außen rum, was ja die Schwierigkeit ist. Da das aber ein integriertes System ist, kann und will ich CPU, Speicher und I/O nicht trennen.

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #11 am: 20. March 2010, 10:00 »
Hallo,


Hast Du Dich schon für eine FPGA-Serie entschieden? ...
Überhaupt nicht. Die Begrenzung liegt da im Geld
Oh ja, wenn das Geld-Problem nicht wäre dann würde ich natürlich den Virtex 6 bevorzugen.

ich möchte den ja nirgends einbauen, sondern die CPU selbst entwickeln.
Richtig, ich vergaß. :-)

Register sind aber in TTL sehr, sehr umständlich und teuer zu implementieren,
Ich hab mit der 74-Irgendwas-Reihe schon lange nicht mehr gearbeitet aber ganz normale egde-getriggerte D-FlipFlops mit Enable-Eingang sind doch nun wirklich was ganz normales. Wimre gibt es da z.B. 8 Bit in einem 20 Pin-Gehäuse. Aufwändiger stelle ich mir das Problem der Selektierung eines bestimmten Registers vor um dessen Daten z.B. an die ALU zu übergeben.

Der Aufbau ist möglicherweise:
- 4 Bit Instruktion (=Instruktionsplatine)
- 1 Bit Akkumulatorauswahl (=Ziel)
- 3 Bit Registerauswahl (=Quelle)
Das sieht doch schon gut aus. Wobei 8 Bit eventuell wirklich etwas wenig sind. Sieh Dir mal das http://www.opencores.org/project,zpu an, diese CPU kommt fast ganz ohne Register aus und der Befehlssatz scheint auch sehr simpel zu sein. 442 LUTs von einem Spartan 3 ist so wenig das sollte sich mit vertretbarem Aufwand in TTL realisieren lassen. Bestimmt kann man dieses Konzept noch etwas nach unten skalieren wenn man keine richtige 32 Bit CPU will.

Wahrscheinlich werde ich aber 5 Bit Instruktion vorsehen müssen, und bei einer 12-Bit-Architektur habe ich auch wesentlich mehr Spielraum für die Codierung.
Du weißt das Du mit einer 12 Bit Architektur zu nichts auf der Welt kompatibel bist? Nichtmal zu den üblichen C-Compilern. Oder Du verschwendest bei den Daten immer 4 Bit. Vielleicht wäre für Dich auch eine Harvard-Architektur (so wie die 8 Bit AVRs) interessant.

.... dann sollte ich aber lauffähigen (und krass unperformanten) Code hinkriegen. Flatmem-Simulation eben.
Das erinnert mich an den Output von C-Compilern für den x86 Real-Mode bei großen Programmen, da musste auch für fast jeden zweiten Speicherzugriff ein Segmentregister geladen werden (es sind ja nur DS und ES frei verfügbar). Performance ist wirklich was anderes. Ich will ja auch Segmentierung aber da ich 16 Segmentregister vorgesehen hab und die Segmente volle Größe erreichen können denke ich mal werden solche Dinge auf meiner Architektur keine Performance kosten.

Vielleicht muss ich später auch einen C-Ableger basteln, wo ich dem Compiler die RAM-Verwaltung abnehme und den Teil per Hand machen muss.
Das würde ich mir an Deiner Stelle gut überlegen. Ich verstehe zwar nicht ganz was Dir da vorschwebt aber ich vermute das für sowas mal problemlos ein Mann-Jahr drauf geht (in Sonntag-Nachmittage umgerechnet bist Du dann ziemlich lang beschäftigt).

Prinzipiell orientiere ich das OS an der Hardware statt umgekehrt.
Also ich versuche CPU- und Plattform-Architektur mit dem OS als eine Einheit zu betrachten. Die HW soll die OS-Konzepte möglichst gut (und performant) unterstützen und das OS soll die Fähigkeiten der HW optimal ausreizen. Die HW wird bei mir nichts unterstützen was das OS nicht sinnvoll nutzen kann und das OS wird nichts voraussetzen was sich in HW nicht vernünftig umsetzen lässt.

Da das aber ein integriertes System ist, kann und will ich CPU, Speicher und I/O nicht trennen.
Da haben wir etwas unterschiedliche Design-Ziele. CPU-Architektur und Plattform-Architektur sollen bei mir nur aus SW-Sicht aus einem Guss sein, aus HW-Sicht sind es mehr oder weniger unabhängige Teil-Systeme mit einer klar definierten Schnittstelle.


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

chris12

  • Beiträge: 134
    • Profil anzeigen
Gespeichert
« Antwort #12 am: 20. March 2010, 11:32 »
meine cpus sind mehr oder minder sehr simpel
die erste hab ich in logisim entwickelt und konnte ganze 16Byte Ram und 256Byte Rom ansprechen. im Rom lag der einfachheit halber der programcode und im ram wurde linear das ergebnis der aluoperation abgelegt. allerdings konnte man keine daten aus dem ram lesen, aber ich war schon happy, dass es überhaupt irgendwie funktioniert hat. die cpu, kpu mit namen, war eine 4bit cpu mit einem 16bit breiten befehlssatz. die 4bit für daten resultierten daraus, dass alle operationen in diesen 16bit einzeln untergebracht wurden, da ich noch nichts vom decodieren gehört hatte.
ein befehl war also so aufgebaut:
x x x x x x x x x x x x x x x x
| | | | | | | | | | | | | | | |
 -   - - -   -   -   -   - - -
 |     |     |   |   |     |
Addr  Addr  Alu   Reg     Data
 OP         OP   CB  CA
 
das bedeutet ein
mov r1, 4 wurde zu 0000000000100100
und leider musste aus irgenteinem grund nach jeder aluop ein null word kommen, da sonst falsche ergebnisse in den ram geschrieben werden.
btw hatte kpu auch keinerlei jmp operationen, nicht mal einen einfachen sprung, obwohl es vorgesehen war. sie hatte 3 register r0, r1 und r2, die alle allzweck waren

die weiterentwicklung war KPU Mk 2, oder einfach KPUII. diese sollte schon besser sein. sie hatte zwar immernoch die 16bit instructionen, aber konnte schon 8bit daten verarbeiten.
eine instruction war also so aufgebaut:
x x x x x x x x x x x x x x x x
| | | | | | | | | | | | | | | |
Enable   - - -   - - - - - - -
| | | |    |           |
J A  R     OP         Data
m L  e
p U  g
    | |
Write Read
aus einem mov r1, 4 wurde jetzt 0010000100000100
kpuII wurde in logisim die wirklich fertiggestellt, aber dafür hab ich meinen ersten emulator in c# geschrieben KPUIIEMU ;)
kpuII hatte 16 allzweckregister. jump operationen wie
jmp
je
jne
ja
jb
und konnte auf den ram und den rom dierekt zugreifen, zwar nur mit festwerten, aber immerhin. der programmcode lag immernoch im rom. alletdings hatte diese cpu schon einen screen port und einen keyboard port, die allerdings nie in logisim sorichtig funktioniert haben. aber dafür waren die befehle read (register) und disp (festwert | ram:[(adresse)] | rom:[(adresse)] | register) vorgesehen.

meine nächste cpu, die ich erst auf dem papier und dann in logisim umsetzen wollte, war die 140A, eine richtige multibyte 8bit cpu. daten und code lagen im ram und sogut wie alle operationen sind multibyte bishin zu 3 byte. diese cpu ist in logisim nie sorichtig fertig geworden, da ich mit probleme mit den multibyte instruktionen hatte.
die 140A hatte einen 8bit breiten adressbus und konnte somit 256byte an ram ansprechen. sie hatte 4 allzweckregister, einen ip und ein flagregister.
die instruktionen waren so aufgebaut
xxxx | xxxx | OP
-----|------|---------
0000 | 0000 | HLT
0001 | 00 # | r# = <IP+1>
0001 | 0100 | [<IP+1>] = <IP+2>
0001 | 10 # | r# = [<IP+1>]
0001 | 11 # | [<IP+1>] = r#
-----|------|---------
0111 | #1#2 | cmp r#1, r#2
0110 | 00 # | cmp r#, <IP+1>
0110 | 01 # | cmp r#, [<IP+1>]
-----|------|---------
0011 | 0000 | jmp <IP+1>
0011 | 0001 | je <IP+1>
0011 | 0010 | jne <IP+1>
0011 | 0011 | ja <IP+1>
0011 | 0100 | jb <IP+1>
0011 | 0101 | jna <IP+1>
0011 | 0110 | jnb <IP+1>
-----|------|---------
1000 | #1#2 | r#1 = r#1 + r#2
1001 | #1#2 | r#1 = r#1 - r#2
1010 | #1#2 | r#1 = r#1 * r#2
1011 | #1#2 | r#1 = r#1 / r#2
1100 | #1#2 | r#1 = r#1 & r#2
1101 | #1#2 | r#1 = r#1 | r#2
1110 | 00 # | r# = ~r#
1111 | #1#2 | r#1 = r#1 XOR r#2
wie man erkennen kann recht simpel.
flags gab es nur 3 stück: das gleichheitflag, größerflag und kleiner flag.
aus mov r0, 0xF0 wird also 00010000 11110000
einige kleinigkeiten, wie zum beispiel adressierung über register, müssen über kleinere hacks gemacht werden. wodurch dann selbstmodifizierender code entsteht.

die letzte cpu und die auch größte in meinen projekt umfang ist die XCPU, warum die so heißt weiß ich leider auch nicht.
diese cpu arbeitet mit sicherheit nicht mehr nach dem RISC prinziep sondern eher im gegenteil. sie hat einen 32 bit adressbus kann mit 32bit daten umgehen und eine ram speicherzelle hat 32 bit. somit kann diese cpu 137438953472 bit insgesamt ansprechen was  16 GB ergeben. leider kann der emulator nur ein zwanzigstel, also ca 0.8 GB, davon bereit stellen, da meine maschiene nicht mehr hergibt.
es giebt 16 register, die allzweck benutzt werden könnten, aber vom assembler für call oder stack operationen genutzt werden. die cpu beherscht diese nähmlich nicht und, desshalb habe ich diese hacks gleich in den assembler eingebaut.
die cpu kann teoretisch 2^32 in und out ports haben allerdings werden vom emulator vorerst nur 0-2 unterstützt. flags gibts 32 aber nur 4 existieren, die drei der 140A und das zero flag. interrupt, die es bei den anderen nicht gab, sollen auch implementiert werden, allerdings muss ich mir darüber nochmal gedanken machen.

zum assembler:
ich hab fast für jede meiner cpus eien eigenen assembler, in c#, geschrieben, außer für die 140A. die syntax ist für alle dem intelsyntax der x86 ähnlich, nur die register heißen anders r0-r(2, 3 oder 15 bzw 9). also mov (ziel), (quelle). bei der xcpu werden allerdings auch operationen wie add r1, r2, r3 möglich sein (r3 = r1 + r2).
der assembler für die xcpu ist noch nicht ganz fertig, aber es gibt schon labels, jmp, data anweisung, org anweisung, times anweisung und die $ anweisung.

wenn noch fragen sind beantworte ich die gerne
btw wenn ich in den rechnungen(bit -> GB) einen fehler hab korrigiert mich bitte
OS? Pah! Zuerst die CPU, dann die Plattform und _dann_ das OS!

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #13 am: 20. March 2010, 15:24 »
Hallo,


meine cpus sind mehr oder minder sehr simpel
Mag sein, aber immerhin tun sie was, Respekt!

.... wodurch dann selbstmodifizierender code entsteht.
Pfui, schäm Dich! :wink:
Sowas unanständiges macht man heute doch nicht mehr. AMD und Intel verkaufen uns das NX-Flag als Anti-Virus-Flag und Du erhebst modifizierbaren Code zum Design-Ziel. Wenn ich mal wirklich Langeweile hab schreib ich fix nen Virus für Deine CPU. :-D

ich hab fast für jede meiner cpus eien eigenen assembler, in c#, geschrieben
Wie viel Arbeit hat das den in etwa gemacht bzw. wie viel Quell-Code ist das ungefähr?

also mov (ziel), (quelle). .... wie add r1, r2, r3 möglich sein (r3 = r1 + r2).
Das passt irgendwie nicht zusammen, sollte es nicht "r1 = r2 + r3" heißen?

der assembler für die xcpu ist noch nicht ganz fertig, aber es gibt schon labels, jmp, data anweisung, org anweisung, times anweisung und die $ anweisung.
Wie verwaltest Du die Befehle intern? Wie löst Du die Labels auf?

Ich nutze dafür ganz einfach vector (in C++). Wenn der Code dann vollständig eingelesen ist verteile ich alles auf die einzelnen Sektionen/Segmente (.code , .const , .data usw.), alle Nicht-Code-Segmente werden dann "assembliert" damit dort alle Labels fest stehen. Anschließend werden die Code-Segmente assembliert. Dazu gibt es eine Vorrunde in der bei allen Befehlen geprüft wird wie groß sie mindestens werden damit jeder Befehl ein Initial-Offset bekommen kann. Dann soll der gesamte Code so lange immer wieder assembliert und optimiert werden bis sich das best-mögliche Ergebnis eingestellt hat (das Abbruchkriterium wird mir sicher einiges an Kopfzerbrechen bereiten). Viele Befehle können in kleinerer Ausführung benutzt werden, vor allem Sprünge müssen nur sehr selten in der 60 Bit Version (damit kann in 64 TByte Code vor und in 64 TByte Code zurück gesprungen werden) vorliegen für eine kurze Schleife, die sich vielleicht nur über 30 Assemblerbefehle erstreckt, reichen auch die 20 Bit Sprünge (damit kann noch in +/- 32 kByte Code vor und zurück gesprungen werden). Aufgrund meiner sehr komplexen Befehls-Architektur wird nicht nur der Decoder in der CPU reichlich fett werden sondern auch der Encoder (Code-Generator) im Assembler. Linken möchte ich aus diesem Grund noch vor dem Assemblieren damit der Code-Generator wirklich über das gesamte Programm optimieren kann. Das damit ein Compile-Lauf nicht gerade als schnell gilt ist mir klar (ist ja z.B. beim Itanium nicht anders), aber Ausführungsgeschwindigkeit ist mir schon ein wichtiges Kriterium.

Von der Funktionalität meiner Plattform möchte ich schon in die Nähe aktueller PCs kommen, bei der Performance kann ich mich wahrscheinlich schon bei einem Hundertstel glücklich schätzen. Ich hoffe ich erreiche im FPGA 50 MHz Takt für den CPU-Kern, damit möchte ich dann performancemäßig etwa mit einem 100 MHz 486 gleich ziehen.


@Svenska: Hast Du eigentlich schon über Interrupts (als INT-Befehl für die SW für SYSCALLs oder als IRQ von der Peripherie) nachgedacht?


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

chris12

  • Beiträge: 134
    • Profil anzeigen
Gespeichert
« Antwort #14 am: 20. March 2010, 18:11 »
Zitat von: erik.vikinger
Das passt irgendwie nicht zusammen, sollte es nicht "r1 = r2 + r3" heißen?
ist eigentlich egal. zudem noch ist es nicht im assembler implementiert, da können die operanden immernoch vertauscht werden ;)

zum selbstmodifizierenden code:
es war kein design ziel, sondern ist eine notwendigkeit aufgrund der einfachheit ;)

zu den assemblern:
kpuasm: nur 281 zeilen (könnten aber reduziert werden)
kpuIIasm: 564 zeilen in 3 dateien (könnten auch reduziert werden)
xasm: 842 zeilen (werden noch mehr werden)

zu den befehlen:
ich mach das über eine böse variante, über string split und dann mit regex vergleichen, da ich da noch nichts vom ordentlichen compilerbau/assemblerbau gehört hab.
wenn man jetzt folgenden xasm code hat
data 0x10000000, :hallo ;mov r0, :hallo
data 0x10020000, $+3 ;mov r2, $+3 )
jmp :start           ;            ) call :start
jmp $-1
start:
data 0x20310000    ;mov r1, [r0]
data 0x01110000, 0  ;out 0, r1
data 0x30100000, 1 ;add r0, 1
cmp r1, 0
jne :start
jmp r2  ;ret
hallo:
data "Hallo, Welt!",10,13, 0
wird zuerst folgender objekt code in stage 1 generiert
10000000 :hallo
10020000 $+3
40100000 :start
40100000 $-1
start:
20310000
01110000 00000000
30100000 00000001
00210000 00000000
42100000 :start
40020000
hallo:
00000048 00000061 0000006C 0000006C 0000006F 0000002C 00000020 00000057 00000065 0000006C 00000074 00000021 0000000A 0000000D 00000000
dann werden die labels eingelesen und in einer KeyValue liste abgespeichert zusammen mit ihren adressen, bei bedarf noch der org wert drauf addiert, und danach werden die labels mit ihrer adresse ersetzt.
dann hat man diesen code 10000000 00000212
10020000 00000206
40100000 00000208
40100000 00000206
20310000
01110000 00000000
30100000 00000001
00210000 00000000
42100000 00000208
40020000
00000048 00000061 0000006C 0000006C 0000006F 0000002C 00000020 00000057 00000065 0000006C 00000074 00000021 0000000A 0000000D 00000000
der wirt dann einfach in die entsprechenden hexwerte umgewandelt und in die ausgabe datei geschreiben.

zum aufwand:
je komplexer die cpu bzw deren befehlssatz, desto komplexer der assembler. und xasm ist immernoch nicht fertig.

ich hoffe ich hab deine fragen damit beantwortet
OS? Pah! Zuerst die CPU, dann die Plattform und _dann_ das OS!

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #15 am: 20. March 2010, 18:20 »
Hallo,

dass eine 12-Bit-CPU inkompatibel zur Welt ist, ist mir durchaus bewusst. Das war auch die Ursprungsidee, aus der sich alles entwickelt hat. :-o

Mit dem jetzigen Ziel habe ich eine recht hohe Ausführungsgeschwindigkeit, brauche aber viele Befehle für gewisse Aktionen. Angenommen, ich muss ständig die Bänke wechseln, das kostet jeweils 5-10 Takte zusätzlich. Ein 8086 braucht aber allein für die Adressberechnung zwischen 7 und 12 Taktzyklen plus die jeweilige Ausführungszeit des Befehls (2..17, bei MUL/DIV in die Hunderte), ein 8088 durch den 8-Bit-Datenbus (und die kleinere Prefetch-Queue) im Schnitt ein Drittel mehr.

Mit dem C-Ableger hatte ich eher vorgestellt, dass ich die Overlays selbst basteln muss und im Betriebssystem dann automatische Bankwechsel o.ä. vorsehe, so dass die Anwendungsentwicklung etwas einfacher wird. Also keine eigene Programmiersprache, das ist bissl heftig.

Eine Hardvard-Architektur erfordert zusätzliche Lade-/Schreibbefehle für den Codespeicher, um Code von externen Medien nachladen zu können. Außerdem müsste ich dann zwei Speichercontroller implementieren oder beide Speicher in einen gemeinsamen Speicher stecken (und verwalten). Die Befehle kann ich nicht länger gestalten als die Busbreite, da ich sonst zwei Zyklen zum Einlesen bräuchte.

Über Interrupts habe ich bisher noch nicht nachgedacht, möglicherweise realisiere ich das über ein Flag (und damit Polling); ein Befehl ist nicht vorgesehen. Software-Interrupts kann ich mit JMP auf Kosten eines Registers realisieren; Hardware-Interrupts möglicherweise auch.

Die Registerauswahl kostet mindestens zwei Gatterlaufzeiten, selbst wenn ich die Gatter bereits direkt implementiere, und in den Opcodes habe ich (bei 8 Bit) nur Platz für 8 Register...

Gruß,
Svenska

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #16 am: 20. March 2010, 20:01 »
Hallo,


zum selbstmodifizierenden code:
es war kein design ziel, sondern ist eine notwendigkeit aufgrund der einfachheit ;)
schon klar
Auf allen modernen CPUs ist das modifizieren von Code verboten, nur bei x86 macht man extra Anstrengungen bei der Verwaltung des Code-Cache um sowas zu ermöglichen obwohl kein modernes OS das benötigt oder auch nur zulässt.

zu den assemblern:
kpuasm: nur 281 zeilen (könnten aber reduziert werden)
kpuIIasm: 564 zeilen in 3 dateien (könnten auch reduziert werden)
xasm: 842 zeilen (werden noch mehr werden)
Ah ja, bei mir sind es momentan 150 kByte (Zeilen weiß ich jetzt nicht, muss ich mir mal ein passendes Tool besorgen) in 17 Dateien, wird aber noch deutlich mehr werden. Für die Immediate-Parameter von Befehlen hab ich einen Literal-Parser der schon allein über 1200 Zeilen hat, der kann momentan mit Zahlen in Binär, Dezimal und Hexadezimal umgehen (in C-Syntax und VHDL-Syntax). Des weiteren parst er ganze Gleichungen mit Klammern und Operatoren (+ - * / % | & ^ ~) mit Vorrangregeln, Label sind da aber noch nicht richtig implementiert. Gleitkommazahlen fehlen auch noch. Aus den Gleichungen wird ein Baum generiert welcher dann zusammengerechnet wird, bis jetzt kommt immer eine eindeutige Zahl raus weil ja noch die Labels fehlen. Dieser Baum wird dann ins Befehlsobjekt abgelegt und beim assemblieren mit den Label-Adressen ausgerechnet.

zu den befehlen:
ich mach das über eine böse variante, über string split und dann mit regex vergleichen, da ich da noch nichts vom ordentlichen compilerbau/assemblerbau gehört hab.
Naja, jeder fängt mal klein an. In meinem Assembler sind auch einige Stellen die ich noch mal ordentlich machen möchte.

je komplexer die cpu bzw deren befehlssatz, desto komplexer der assembler.
Da schreibst Du wahr!


Mit dem jetzigen Ziel habe ich eine recht hohe Ausführungsgeschwindigkeit, brauche aber viele Befehle für gewisse Aktionen.
Das ist ein generelles CPU-Design-Problem, entweder kleine und schnelle Befehle von dehnen man aber viele für einfache Aufgaben braucht oder mächtige Befehle die aber langsamer sind und auch mehr OpCode-Platz brauchen.

Angenommen, ich muss ständig die Bänke wechseln, das kostet jeweils 5-10 Takte zusätzlich.
Ja ich verstehe. Mehr Bänke gleichzeitig kosten Dich mehr Logik. Ich will nicht mit Dir tauschen. :roll:

Mit dem C-Ableger hatte ich eher vorgestellt, dass ich die Overlays selbst basteln muss und im Betriebssystem dann automatische Bankwechsel o.ä. vorsehe, so dass die Anwendungsentwicklung etwas einfacher wird.
Ich verstehe immer noch nicht was Du meinst.

Eine Hardvard-Architektur erfordert zusätzliche Lade-/Schreibbefehle für den Codespeicher
Und dafür fehlen Dir wertvolle Bits in den Befehlen.

Über Interrupts habe ich bisher noch nicht nachgedacht, möglicherweise realisiere ich das über ein Flag (und damit Polling); ein Befehl ist nicht vorgesehen. Software-Interrupts kann ich mit JMP auf Kosten eines Registers realisieren; Hardware-Interrupts möglicherweise auch.
Polling ist aber was anderes als Interrupts. Naja, Du wirst wissen was Du tust.


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #17 am: 20. March 2010, 21:40 »
Mit dem C-Ableger hatte ich eher vorgestellt, dass ich die Overlays selbst basteln muss und im Betriebssystem dann automatische Bankwechsel o.ä. vorsehe, so dass die Anwendungsentwicklung etwas einfacher wird.
Ich verstehe immer noch nicht was Du meinst.
Das Problem mit dem Banking bleibt ja erhalten, so oder so; wenn ich aber in der C-Library (bzw. im Kernel) entsprechende Unterstützung aber vorsehe, dann wird es vielleicht etwas weniger schmerzhaft. Die Programme würden dann nur noch eine Aufteilung in Overlays bekommen, sich aber nicht darum kümmern müssen, an welcher Adresse und in welcher RAM-Bank die benötigten Daten nun gerade liegen.

Schmerzhaft wird es ohnehin, da hab ich wenig Illusionen.

Eine Hardvard-Architektur erfordert zusätzliche Lade-/Schreibbefehle für den Codespeicher
Und dafür fehlen Dir wertvolle Bits in den Befehlen.
Eben.

Über Interrupts habe ich bisher noch nicht nachgedacht, möglicherweise realisiere ich das über ein Flag (und damit Polling); ein Befehl ist nicht vorgesehen. Software-Interrupts kann ich mit JMP auf Kosten eines Registers realisieren; Hardware-Interrupts möglicherweise auch.
Polling ist aber was anderes als Interrupts. Naja, Du wirst wissen was Du tust.
Das war eher entweder-oder gemeint. Möglicherweise realisiere ich keine Hardwareinterrupts (d.h. Pollingbetrieb), möglicherweise lässt sich eine simple "sichere PC nach Register, JMP an fixe Adresse" einfach realisieren, dann tue ich das natürlich. Inwieweit meine CPU überhaupt Hardwarestacks unterstützt, weiß ich bisher noch nicht; nested function calls und Syscalls sind dann halt wieder softwareseitig (z.B. Assemblermakro) realisiert.

Genau das ist aber der Grund, weswegen es mich interessiert, wie andere an solche Projekte herangehen - denn an Interruptverarbeitung habe ich wirklich noch nicht gedacht.

Wenn man die CPU hinreichend blöde macht, muss man halt alles in Software nachimplementieren. Im Extremfall kommt dann halt 'ne Turingmaschine bei raus, aber bissl mehr möchte ich schon können... außerdem programmiert sich eine Turingmaschine wirklich unangenehm. :-D

Gruß,
Svenska

chris12

  • Beiträge: 134
    • Profil anzeigen
Gespeichert
« Antwort #18 am: 20. March 2010, 21:46 »
also wenn ich schätzen sollte hast du so ca 4000 zeilen in deinem assembler ...
OS? Pah! Zuerst die CPU, dann die Plattform und _dann_ das OS!

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #19 am: 20. March 2010, 22:51 »
Hallo,


Schmerzhaft wird es ohnehin, da hab ich wenig Illusionen.
Dann is ja gut wenn Du weißt auf was Du Dich da einlässt. :wink:

Das war eher entweder-oder gemeint. Möglicherweise realisiere ich keine Hardwareinterrupts (d.h. Pollingbetrieb), möglicherweise lässt sich eine simple "sichere PC nach Register, JMP an fixe Adresse" einfach realisieren, dann tue ich das natürlich. Inwieweit meine CPU überhaupt Hardwarestacks unterstützt, weiß ich bisher noch nicht;
Da gibt es IMHO 2 Wege: einmal werden für den IRQ-Mode (teilweise) andere Register benutzt so das Du nichts sichern musst aber noch mehr Register brauchst (ARM z.B. geht diesen Weg) oder Du hast einen HW-Stack und die CPU kann "sichere PC auf Stack und springe nach Irgendwohin" (x86 ist da ein klassisches Beispiel). Ich gehe den ARM-Weg aber in etwas anderer Form. ARM hat für jeden System-Mode (IRQ, Exception, SYSCALL ...) eigene Schatten-Register so das man diese Modi (theoretisch) beliebig verschachteln kann, soll heißen das man im SYSCALL-Handler noch ne Page-Not-Present-Exception verarbeiten kann und im Page-Not-Present-Handler könnte noch ne Illegal-Opcode-Exception kommen ohne das da was kollidiert (in echt begrenzt ARM das natürlich auf sinnvolle Möglichkeiten), das kostet aber viele Register (ARM hat über 30 HW-Register obwohl die SW immer nur 16 sehen kann). Ich möchte nur einen Register-Schatten (und auch nur für R56 bis R63) haben was aber bedeutet das im SYSCALL-Handler keine Exception auftreten darf und auch keine IRQs angenommen werden können. Aus einem System-Mode muss die CPU immer erst in den User-Mode zurück um dann wieder einen anderen System-Mode betreten zu können. Ist zwar ne Einschränkung aber ich denke damit kann ich gut leben und es spart reichlich Register. 64 normale Register sind schon ne Menge Zeugs, das kostet auch einiges an Logik-Ebenen bzw. Fan-Out und reduziert im Endeffekt den maximalen Takt für meine CPU.

Genau das ist aber der Grund, weswegen es mich interessiert, wie andere an solche Projekte herangehen - denn an Interruptverarbeitung habe ich wirklich noch nicht gedacht.
Ich vermute mal dieser Thread wird noch ne Weile benutzt werden. :-D

Wenn man die CPU hinreichend blöde macht, muss man halt alles in Software nachimplementieren.
Es kommt eben auf das richtige Verhältnis an. Was wirklich das Optimum ist merkt man aber erst wenn die ersten großen Programme laufen, bzw. dann stellt man eventuell fest das man nicht das Optimum getroffen hat.


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

 

Einloggen