3 Punkte von GN⁺ 2024-04-10 | 1 Kommentare | Auf WhatsApp teilen
  • Ein Hardware-Baubericht über die Erweiterung eines Computers aus diskreter Logik um 10BASE-T-Ethernet-Sende- und Empfangsfunktionen – ohne kommerzielle CPU oder dedizierten Netzwerkchip
  • Auf einem zuvor gebauten 10BASE-T↔SPI-Physical-Layer-Adapter wurde ein MAC-Layer-Modul ergänzt, um ihn mit einem Homebrew-Computer zu verbinden; Sender und Empfänger sind als unabhängige Full-Duplex-Struktur ausgelegt
  • Der Empfänger wandelt SPI-Daten in Bytes um, speichert sie in 2 kB SRAM und prüft die ersten 6 Bytes in Hardware, sodass nur FE:FA:F6:F2:EE:EA oder Broadcast-MACs akzeptiert werden
  • Um die Schaltung zu vereinfachen, überlässt der Sender FCS-Erzeugung und Präambel-Vorbereitung der Software und unterstützt nur Frames fester Länge von 1024 Bytes
  • Es wurde sogar ein C-Compiler gebaut, der uIP 1.0 kompilieren kann, um Netzwerk-Apps auszuführen; das Ergebnis liegt bei durchschnittlich 85 ms Ping und 2,6 kB/s beim Download statischer HTTP-Dateien

Ethernet an einen Computer aus diskreter Logik anschließen

  • Als Fortsetzung des Projekts, ein vollständiges Computersystem aus diskreten Logikbausteinen zu bauen, wurde ein Ethernet-Adapter implementiert, der Netzwerk-Anwendungen ausführen kann
  • Zuvor wurde ein Physical-Layer-Adapter gebaut, der 10BASE-T-Ethernet-Signale in SPI umwandelt und zurück; für die damaligen Funktionstests wurde ein STM32-Mikrocontroller verwendet
  • Der Kern dieser Arbeit ist ein MAC-Layer-Modul, um diesen Adapter mit dem Homebrew-Computer zu verbinden
  • Der Adapter ist als Full-Duplex-Struktur aufgebaut; Sende- und Empfangsteil arbeiten unabhängig voneinander

Empfänger: SPI-Daten im Frame-Buffer speichern

  • Der Empfänger wandelt serielle SPI-Daten in byteweise parallele Daten um und extrahiert den Byte-Takt
  • Die Ziel-MAC-Adresse wird in den ersten 6 Bytes geprüft; Frames, die nicht den Kriterien entsprechen, werden verworfen
  • Empfangene Bytes werden in einen 6116-2-kB-SRAM-Puffer geschrieben
  • Wenn ein Frame endet, wird der Empfänger deaktiviert und nimmt keine weiteren Frames an, bis er wieder aktiviert wird
  • Der Byte-Zähler behält seinen Wert auch nach dem Anhalten bei, sodass die CPU die Empfangslänge auslesen kann
  • FCS wird nicht in Hardware geprüft
  • Datenerfassung und Buffer-Zugriff

    • Serielle SPI-Daten gelangen in das Schieberegister U32; U30 und U31 zählen jeweils Bits und Bytes
    • Das D-Flipflop U29B erzeugt das SRAM-Schreibsignal recv_buf_we; dieses Signal wird bei jeweils 8 Bit Eingangsdaten kurz auf Low gezogen
    • Empfangene Bytes werden in den 6116-SRAM U20 geschrieben
    • U13, U16 und U18 bilden den Adressmultiplexer und wählen als SRAM-Adresseingang entweder den Byte-Zähler oder den System-Adressbus
    • U21 dient als Tri-State-Buffer, der empfangene Bytes an den RAM weitergibt
    • Damit die CPU auf Empfangsdaten und Länge zugreifen kann, sind RAM und Byte-Zähler mit dem System-Datenbus verbunden
    • U25 verbindet den Empfangs-RAM mit dem System-Datenbus
    • Nach Abschluss eines Frames bleibt der Wert des Byte-Zählers auf dem Bus recv_byte_cnt erhalten
    • U26 und U27 legen diesen Wert auf den System-Datenbus, wenn eine Leseanforderung an eine bestimmte Adresse eingeht
    • Die übrige Hälfte von U27 bildet ein 2-Bit-Read-only-Statusregister zum Abfragen des Empfänger- und Senderstatus

Hardware-MAC-Adressfilterung

  • Bei der Analyse des Ethernet-Traffics zeigte sich, dass Frames meist in kleinen Gruppen von 3 bis 4 mit kurzen Verzögerungen dazwischen eintrafen und dass selbst innerhalb derselben Gruppe oft unterschiedliche Ziel-MAC-Adressen vorkamen
  • Da der Computer möglicherweise nicht schnell genug wäre, um MAC-Filterung in Software auszuführen und den Empfänger wieder zu aktivieren, war Hardware-MAC-Filterung nötig
  • Ein Ansatz, eine benutzerdefinierte MAC-Adresse zu speichern und mit den ersten 6 Bytes zu vergleichen, wurde als zu komplex verworfen
  • Eine MAC-Adresse aus einem einzelnen wiederholten Byte wäre ebenfalls möglich gewesen, letztlich wurde die MAC-Adresse jedoch als Funktion des Byte-Index erzeugt
    • Bit 0 ist fest 0
    • Bit 1 ist fest 1
    • Bit 2–4 sind der invertierte Wert des Byte-Index
    • Bit 5–7 sind fest 1
  • Die nach dieser Regel erzeugte MAC-Adresse lautet FE:FA:F6:F2:EE:EA
  • Für ARP wird auch die Broadcast-MAC FF:FF:FF:FF:FF:FF akzeptiert
  • U33 vergleicht Datenbit 0 und Bit 2–4 mit den gewünschten Werten; der Ausgang von U34A wird High, wenn diese Bits übereinstimmen
  • U35A implementiert die Broadcast-MAC-Prüfung; der Ausgang wird High, wenn Bit 0 und Bit 2–4 alle 1 sind
  • Die beiden Signale werden per Dioden-OR mit D7 und R6 kombiniert, und U35B prüft, ob die übrigen Bits alle 1 sind
  • Das Gültigkeitsergebnis eines einzelnen Bytes wird in U10A akkumuliert
    • Wenn kein Frame empfangen wird, ist das eingehende SPI-Slave-Select-Signal ss Low und U10A wird auf 1 gesetzt
    • Während des Frame-Empfangs wird der Wert bei jedem empfangenen Byte aktualisiert
    • Wenn die Ziel-MAC-Adresse den Kriterien entspricht, bleibt der Wert von U10A High
    • Sobald die Byte-Adresse 5 erreicht, wird der Endwert in U36B gelatcht; stimmt die Zieladresse nicht, wird der Frame-Empfang blockiert

Sender: Schaltung durch Frames fester Länge vereinfachen

  • Wie beim Empfänger wird auch beim Sender die FCS-Erzeugung nicht in Hardware implementiert, sondern in Software erledigt
  • Um die Schaltung zu verkleinern, unterstützt der Sender nur Frames fester Länge
  • Als Frame-Länge wurden 1024 Bytes gewählt, ein Wert nahe der üblichen MTU von 1500 Bytes
  • Die für 10BASE-T nötige Präambel besteht aus mehreren 0x55 und einem abschließenden 0xD5; die Software muss sie zusammen mit dem Frame in diese 1024 Bytes laden
  • Die feste Frame-Länge beeinflusst Protokolle höherer Schichten nicht
    • Protokolle höherer Schichten kodieren die Paketgröße im Header
    • Sie hängen nicht von der tatsächlichen Ethernet-Frame-Länge ab
  • Sendedatenfluss

    • Sendedaten werden im SRAM gespeichert
    • Ein 20-MHz-Takt wird einem 4-Bit-Zähler zugeführt, dessen Overflow-Ausgang als Byte-Takt verwendet wird
    • Das Schreiben eines Werts an eine bestimmte nur schreibbare Speicherposition aktiviert den Zähler und startet die Frame-Übertragung
    • Parallele Byte-Daten werden über ein Schieberegister serialisiert
    • Wie beim Empfänger zählt U12 die Bits und U14 die Bytes
    • Der 20-MHz-Takt kommt von einem integrierten Oszillator und wird nicht direkt verwendet, sondern mindestens durch 2 geteilt
    • Dadurch beeinflusst das Tastverhältnis des Oszillators das Ausgangssignal nicht
  • RAM, Schieberegister und Timing

    • Für die Auswahl des Adresseingangs von RAM U22 werden wie beim Empfänger drei 74HC157-Multiplexer verwendet
    • U23 wird genutzt, um Daten in den RAM zu laden
    • U24 dient als Zwischenspeicher für das aktuell gesendete Byte
    • Der Byte-Zähler 74HC4040 ist ein Ripple-Counter und stabilisiert sich daher langsam
    • Solange der RAM-Ausgang noch nicht gültig ist, liefert U24 einen stabilen Ausgang
    • Die Daten gelangen in das Schieberegister U28 und werden bitweise verschoben
    • Wegen eines Hardware-Bugs, bei dem die Bitreihenfolge vom RAM zum Schieberegister falsch verdrahtet war, musste die Software die Bits vertauschen, um das zu umgehen
    • MOSI und SCK müssen exakt synchronisiert sein, um ein gutes 10BASE-T-Signal zu erzeugen
    • U11A und U8B übernehmen diese Synchronisierung
    • tx_cnt0 ist Bit 0 des Bitzählers und wird als durch 2 geteilter 20-MHz-Takt für den Clock verwendet
    • U11A ändert seinen Ausgang passend zu diesem Signal
    • U8B verzögert den Takt, um die von U11A erzeugte Verzögerung auszugleichen
    • D-Latches sind komplexer als einfache AND-Gatter und haben etwa 5 ns mehr Verzögerung, daher wird der schnellere 74LV74A verwendet
    • Der 74LV74A ist der einzige Chip einer schnellen Familie auf diesem Board

CPU-Interface und Memory-Mapping

  • Aus Sicht des Programmierers erscheint der Ethernet-Adapter als memory-mapped Interface
  • Die beiden Frame-Buffer sind auf 0xF000 gemappt
  • Es gibt zwei Read-only-Register
    • Das 8-Bit-Statusregister bei 0xFB00 enthält die Flags RX_FULL und TX_BUSY
    • RX_FULL zeigt an, dass ein Frame vollständig empfangen wurde
    • TX_BUSY zeigt an, dass gerade ein Frame gesendet wird
    • Das 16-Bit-Register bei 0xFB02 enthält die Länge der Empfangsdaten
  • Schreiboperationen werden als Steuerbefehle verwendet
    • Das Schreiben eines beliebigen Werts nach 0xFB00 reaktiviert den Empfänger
    • Das Schreiben eines beliebigen Werts nach 0xFB01 startet die Übertragung
  • Da die CPU keine Interrupts unterstützt, gibt es keine Interrupts
  • Alle zugehörigen Adressen beginnen mit F, also haben die oberen 4 Bits den Wert 1; diese Bedingung prüft U2A
  • Für Buffer-Adressen muss Bit 11 den Wert 0 haben; U1D, D2, R2 und U1E prüfen dies
  • Für Registeradressen muss die zweite Hexadezimalstelle B sein, also 1011; U1B und U2B prüfen dies
  • Die Decoder U4A und U4B dienen zur Auswahl einzelner Funktionen
  • Zwei LEDs zeigen Zugriffe auf Buffer oder Register an

Programmierung und Performance

  • Netzwerkunterstützung war gewünscht, aber weder ein eigener TCP/IP-Stack noch umständliche Assemblerprogrammierung; deshalb wurde ein C-Compiler gebaut
  • Dieser Compiler ist ausgereift genug, um die kleine TCP/IP-Bibliothek uIP 1.0 zu kompilieren
  • Die Code-Dichte der CPU ist sehr niedrig, aber uIP passt in den RAM und lässt auch noch Platz für echte Anwendungen
  • Die Netzwerkleistung ist gering, aber sie wurde ohne kommerzielle CPU oder Spezialchip erreicht
    • Durchschnittliche Ping-Roundtrip-Zeit: 85 ms
    • Download-Geschwindigkeit des HTTP-Servers: 2,6 kB/s
    • Der HTTP-Server stellt statische Dateien von einer SD-Karte bereit
  • Modelle, Schaltplandateien und PCB-Layouts befinden sich im GitHub-Repository

1 Kommentare

 
GN⁺ 2024-04-10
Hacker-News-Kommentare
  • Coole Arbeit, danke fürs Teilen. Besonders gut gefiel mir der Stacktrace des Denkprozesses, und dass vieles aus First Principles hergeleitet oder aus der Perspektive von Einsteigern erklärt wurde, ist didaktisch hervorragend.
    Auch wenn es für echtes Networking unpraktisch ist, sehe ich es nicht nur als Spielerei. In einer Zeit, in der Backdoors in übermäßig komplexen Netzwerkchips gefunden werden, könnte es künftig auch eine ernsthaftere Leserschaft oder Projektmotivation dafür geben.

    • Ich frage mich, wie viele Schwachstellen in modernem Silizium verborgen sind. Selbst in Code mit ein paar Tausend Zeilen tauchen fast täglich Schwachstellen auf; in fest verdrahtetem Silizium stecken im Grunde Mikrochips, die Milliarden Zeilen Code entsprechen.
  • Das ist für einen komplett maßgeschneiderten Computer, was es an sich schon viel beeindruckender macht — ganz zu schweigen von der Stelle „also habe ich einen C-Compiler gebaut“. Trotzdem frage ich mich, wie eine Minimalimplementierung einer Ethernet-Karte für einen „normalen“ PC aussehen würde.
    Vieles dürfte ähnlich sein, und Checksummen könnte man vielleicht von der PC-CPU erledigen lassen. Für die Anbindung bräuchte man entweder rohes Seriell oder, praktikabler, USB; am Ende müsste man wohl einen „echten“ Treiber verwenden oder die Verarbeitung in den Userspace weiterreichen.
    Beim Ansehen ähnlicher Dinge dachte ich, wenn das Gerät https://en.wikipedia.org/wiki/USB_communications_device_clas... implementiert, könnte es ohne eigenen Treiber „einfach funktionieren“; das passt aber wohl nicht gut dazu, alle Checksummen auf der Host-Seite zu berechnen.
    Beim Suchen bin ich auch auf https://en.wikipedia.org/wiki/Ethernet_over_USB gestoßen; vielleicht heißt das, man könnte einen Adapter bauen, der nur die physische Verbindung auf USB umsetzt und den Rest dem Computer überlässt.

    • USB kann man als deutlich komplexer betrachten als das viel ältere 10base2 Ethernet. Wenn man sich per PCIe oder USB mit einem 10base2-Ethernet-Netzwerk verbinden will, ist beides erheblich mehr Arbeit als die Ethernet-Seite.
      Vielleicht ließe sich ein USB-Gerät im FTDI-Stil dazu überreden, 10base2 Ethernet per Bit-Banging zu machen. Man würde nur die „PHY“-Seite implementieren, die den Leitungsverkehr in einen sauberen Bitstream umsetzt und den Frame-Anfang ausrichtet, und den PC den gesamten Rest in Software erledigen lassen.
    • Wenn normale PCs noch wie vor 30 Jahren einen ISA-Bus hätten, könnte ich meine Netzwerkkarte mit nur kleinen Änderungen daran anschließen.
    • Eine NIC in einem FPGA zu implementieren, ist in der Regel zusammen mit einer PCIe-Anbindung sehr üblich.
      Auf der USB-Seite ist CDC-NCM selbst auf praktisch jedem MCU nicht schwer zu implementieren, aber eine USB-HS-PHY-Implementierung braucht faktisch ASIC-Hardware.
      Mit einem USB-HS-ULPI-PHY für 0,30 Dollar ließe sich USB CDC-NCM in einem FPGA wohl ziemlich einfach umsetzen.
  • Am Ende gibt es einen Link zu dem für dieses Projekt gebauten C-Compiler: https://github.com/imihajlow/ccpu-cc
    Es scheint auch einen Linker und libc zu geben. Ich kann nicht wirklich beurteilen, wie komplex das Hardwaredesign ist, aber dass er mal eben einen C-Compiler drangebaut hat, ist beeindruckend.

    • Es ist ein in Rust geschriebener C-Compiler und nutzt für das Parsen der Sprache das Crate lang_c.
  • Wirklich beeindruckend. Solche Projekte würde ich gern selbst ausprobieren, und ich habe großen Respekt vor der Leidenschaft und den unzähligen Stunden, die hineingeflossen sind, um das System zu verstehen und dann zu bauen.
    Ich wünsche mir nicht unbedingt den Ruhestand, aber vielleicht werde ich um diese Zeit herum irgendwann solche Hardware- und Softwareprojekte angehen.

  • Ist es also besser oder schlechter als eine Etherlink 3c501? :-D
    https://mirror.math.princeton.edu/pub/oldlinux/Linux.old/net...
    Wenn ich mich richtig erinnere, konnte ein neues Paket aus dem Netzwerk den Puffer überschreiben, den die CPU gerade lesen wollte. Ich habe sie eine Zeit lang unter Linux benutzt, und die Performance war wirklich schlecht.

  • Interessant fand ich die Stelle: „Eine feste Frame-Länge hat keinen Einfluss auf höhere Protokolle. Höhere Protokolle kodieren die Paketgröße im Header und verlassen sich nicht auf die tatsächliche Ethernet-Frame-Länge.“
    Ich habe vor Kurzem einen Paketdecoder gebaut und in jeder Schicht explizit geprüft, ob die Länge der darunterliegenden Schicht passt. Bei IP muss in meinem Decoder die IP-Datagrammlänge exakt zur Ethernet-Frame-Länge und zur Länge des Link-Layer-Headers passen.
    Das war nicht als Pedanterie gedacht, sondern um kurze Frames zu erkennen; später habe ich beschlossen, auch zu lange Frames als Fehler zu behandeln. Der Autor verwendet uIP, aber ich frage mich, wie Linux oder andere moderne OS damit umgehen. Auch würde mich interessieren, ob er Interoperabilitätstests gemacht hat.

    • Er gibt lange Frames ins Netzwerk aus, und keines der OS, die ich habe, zeigte damit Probleme. Ich habe irgendwo gelesen, dass manche Router tatsächlich lange Frames verwenden, um Metadaten hinter dem Paket zu speichern.
    • Zeitstempel und andere Formen von In-Band-Netzwerktelemetrie werden teils als Trailer in Frames eingefügt. Dabei wird eine neue FCS angehängt.
      Wenn die Anwendung keine L2-Daten sieht, ignoriert der Linux-IP-Stack das einfach.
  • Physisch ist sie viel kleiner als DECs erstes SSI-Ethernet-Kartenset: https://i.ebayimg.com/images/g/NEYAAOSw-mZlg0lZ/s-l1600.jpg
    Die DEC-DEUNA-Boards sind über einen Fuß lang, können aber auch viel mehr. DEUNA ist eine „echte“ NIC: Sie hat Sende- und Empfangsqueues, verarbeitet sie autonom und macht auch DMA. Natürlich hat sie auch ihren eigenen PDP-11 auf der Karte, der das ausführt.

  • Wirklich cool. Ich frage mich, wie lange das gedauert hat.

    • Der Bau des Netzwerkmoduls dauerte etwa einen Monat, aber das Schreiben des Compilers hat deutlich mehr Zeit gekostet.
  • Das zeigt, wie einfach es ist, eine Backdoor in einen Chip einzubauen, der an einen Netzwerkport angeschlossen ist.

  • In einem Kurs zu Communication Systems Engineering habe ich die Ethernet-Signalverarbeitung implementiert und anschließend einen TCP/IP-Stack einschließlich ARP und Switching in Motorola-68k-QUIC-Assembler umgesetzt.
    Das waren die längsten 18 Monate meines Lebens.