- Als erster Schritt, um auf einem Mikrocontroller selbst einen TCP/IP-Stack aufzubauen, wurde versucht, mit einem STM32F401 Nucleo und einem Wiznet-W5100-Shield Ethernet-Frames zu übertragen
- Die Hardware-TCP/IP-Funktionen des W5100 wurden nicht genutzt; stattdessen kam nur der MAC-Raw-Modus zum Einsatz, sodass der Chip lediglich die für die Frame-Übertragung nötige Low-Level-Verarbeitung übernimmt
- Das erste Hindernis entstand dadurch, dass die SPI-Verdrahtung des Arduino Ethernet Shield nicht zum Nucleo passte; nach Prüfung des Routings über den ICSP-Header wurde es durch Modifikationsverdrahtung auf dem Board gelöst
- Danach führten mutmaßliche Chip-Select-Timing-Probleme, ungewöhnliche MISO-Antworten und Garbage-Pakete in Wireshark dazu, dass ein Logic Analyzer und der Vergleich mit einer Referenzimplementierung zu den zentralen Debugging-Werkzeugen wurden
- Die eigentliche Ursache war ein Bug in
w5100_write16(), der das zweite Byte erneut an dieselbe Adresse schrieb; dank eines selbst geschriebenen Analysetools für SPI-Capture-CSV gelang schließlich die korrekte Paketübertragung
Ausgangspunkt für einen eigenen TCP/IP-Stack
- Ziel ist der Start der Serie „Networking from scratch“, in der ein TCP/IP-Stack von Grund auf auf einem Mikrocontroller implementiert wird
- Das sichtbare Ergebnis dieses Schritts ist zwar die erste Ethernet-Paketübertragung, der eigentliche Fokus liegt aber auf der Verfolgung von Bugs zwischen Hardware und Treiber
- Verwendet wurde ein Nucleo-Entwicklungsboard auf Basis des STM32F401
- ARM Cortex-M4
- Betrieb mit bis zu 84 MHz
- 96 KiB RAM
- Wurde als ausreichend Speicher eingeschätzt, um mehrere Pakete vorzuhalten
Die Rolle von Ethernet und W5100
- Ethernet ist nicht nur ein einfacher Port oder ein Frame-Format, sondern eine Familie von Technologien und Standards, die Hardware der physikalischen Schicht, Signalverfahren, Behandlung von Buskollisionen und Frame-Anordnung umfasst
- Da die Ethernet-Signalverarbeitung komplex ist, übernimmt üblicherweise ein dedizierter ASIC die elektrischen Signale auf dem Kabel und verarbeitet Daten auf Frame-Ebene
- Für das Projekt wurde ein Arduino Ethernet Shield mit Wiznet-W5100-Chip verwendet
- Das verwendete Board war ein günstiger Klon, weshalb für den korrekten Betrieb Modifikationen nötig waren
- Der W5100 ist ein Chip, der in einen Ethernet-ASIC einen Hardware-TCP/IP-Stack integriert
- Er stellt 4 „Sockets“ bereit
- Konfigurierbar auf TCP-, UDP-, IP- oder „MAC Raw“-Ebene
- Da ein eigener TCP/IP-Stack implementiert werden soll, werden die TCP/IP-Funktionen des W5100 nicht genutzt; ein einzelner Socket wird nur im MAC-Raw-Modus verwendet
- Der Nutzer übergibt einen Ethernet-Frame
- Der W5100 führt die tatsächliche Übertragung aus
- Preamble und Start-of-Frame-Marker sind Elemente auf elektrischer Ebene und werden vom Chip verarbeitet
- Auch die 32-bit-CRC berechnet der W5100
Problem 1: Die SPI-Signale erreichen den W5100 nicht
- Der Datenaustausch mit dem W5100 erfolgt über SPI
- MOSI: Ausgang des Mikrocontrollers als Hauptchip
- MISO: Ausgang des W5100
- Clock: Referenztakt für Daten
- Chip Select: Signal, dass mit dem abhängigen Chip kommuniziert wird
- Das Datenblatt des W5100 definiert über SPI ein 4-Byte-Befehlsprotokoll
- 1 Byte Operation
- 2 Byte 16-bit-Big-Endian-Adresse
- 1 Byte Wert
- Die Operation ist Schreiben
0xf0oder Lesen0x0f- Andere Werte sind ungültig und sollten ignoriert werden
- Der W5100 gibt bei jedem ausgegebenen Byte einen bekannten Wert auf MISO zurück, sodass Kommunikationsfehler leicht zu erkennen sind
- Bei einem Lesebefehl ist das 4. Byte der aus der angegebenen Adresse gelesene Wert
- Anfangs erschienen auf MISO Garbage-Werte, obwohl Befehle über MOSI gesendet wurden
- Die Ursache war das Design des Arduino Ethernet Shield, das die SPI-Signale nicht auf den Arduino-Standardheader, sondern auf den 6-poligen ICSP-Header routet
- Bei offiziellen Arduino-Boards sind ICSP und die SPI-Signale des Standardheaders intern verbunden, daher gibt es dort kein Problem
- Das Nucleo-Board hat keinen ICSP-Header, sodass die gesendeten SPI-Signale den W5100 nicht erreichten
- Mit einem Multimeter wurde der Widerstand zwischen Stromschienen und SPI-Signalen gemessen, um das Verbindungsproblem zu bestätigen
- Ein unendlicher Widerstand zeigt, dass die Verdrahtung unterbrochen ist
- Mit Lötarbeiten und Kupferlackdraht wurden die Signale zwischen Nucleo und W5100 direkt verbunden
Problem 2: Chip-Select-Timing und ungewöhnliche MISO-Antworten
- Nachdem die SPI-Signale tatsächlich verbunden waren, war Kommunikation mit dem W5100 möglich, und die nächsten Schritte wurden implementiert
- Konfiguration der Raw-Ethernet-Übertragung des W5100
- Setzen der MAC-Adresse
- Konfiguration der TX/RX-Speichersegmente
- Schreiben eines Test-Ethernet-Frames in den TX-Speicher und Auslösen der Übertragung
- Nucleo und Shield wurden per CAT5-Kabel mit einem Laptop verbunden und Wireshark gestartet, aber es war kein Paket zu sehen
- Low-Level-Probleme sind schwer zu verfolgen, weil es keine Fehlermeldungen oder Stack Traces gibt, sondern eher die Form: „Elektronen wurden bewegt, aber das Erwartete ist nicht passiert“
- Zum Debugging wurde ein Logic Analyzer verwendet
- Er sampelt die High/Low-Übergänge digitaler Signale auf mehreren Kanälen
- Software interpretiert Signalbündel wie einen SPI-Bus und zeigt Transaction-Bytes an
- Export in strukturierte Formate wie CSV ist möglich
- Verwendet wurde ein Saleae Logic 8
- Günstige Analyzer für etwa 10 € können ebenfalls mit Saleae Logic2 zusammenarbeiten, beim korrekten und konsistenten Betrieb gibt es jedoch Tradeoffs
- Der erste SPI-Befehl sah korrekt aus
0xf0 0x00 0x00 0x80- Setzt das höchstwertige Bit im Mode Register an Adresse
0x0000, um einen Software-Reset auszulösen - MISO antwortete korrekt von
0x00bis0x03
- Beim nächsten Lesebefehl trat eine ungewöhnliche Antwort auf
- MOSI:
0x0f 0x00 0x00 0x00 - MISO:
0x03 0xff 0xff 0xff
- MOSI:
- Da
0x03der letzte Wert der vorherigen Transaction war, lag ein Problem mit internem Zustand oder Timing des W5100 nahe - Der SPI-Clock lag weit unter dem im Datenblatt angegebenen Maximum von etwa 14 MHz und wurde daher nicht als Ursache betrachtet
- Stattdessen wurde vermutet, dass Chip Select zu schnell wieder high wird und den Chip in einen schlechten Zustand versetzt; daher wurde vor dem Zustandswechsel eine Verzögerung von einigen Mikrosekunden eingefügt
- Danach normalisierten sich die MISO-Antworten
- Auch das erneute Auslesen der Konfigurationswerte ergab plausible Werte
- Die genaue Ursache dieses Problems ist noch nicht vollständig verstanden
- Die Einschränkungen zu Chip Select im SPI-Timing-Diagramm auf Seite 66 des Datenblatts wurden eingehalten
- Die exakten Randbedingungen könnten erneut untersucht werden, derzeit hat jedoch der Fortschritt des Projekts Vorrang
Problem 3: Garbage-Pakete in Wireshark
- Als Wireshark erneut gestartet wurde, erschienen zwar Pakete, aber nicht die beabsichtigten
- In dem Moment, in dem der Mikrocontroller den Sendebefehl ausgab, erschien ein deutlich größeres Raw-Ethernet-Paket, gefüllt mit Garbage-Daten
- Der Plan, die Implementierung nur anhand von Datenblatt und Spezifikation zu schreiben, wurde aufgegeben; in dieser Phase sollte stattdessen mit einer als funktionierend bekannten Implementierung verglichen werden
- Arduino ist nützlich, um schnell eine Referenzimplementierung zu erstellen
- Mit Arduino, Bibliothek und rund 5 Zeilen Code lässt sich komplexes Verhalten überprüfen
- Mehr Zeit kostete es, eine Bibliothek für den Versand von Raw-Ethernet-Paketen zu finden
- Die meisten Arduino-Nutzer versuchen nicht, Netzwerkfunktionen von Grund auf zu implementieren
- Die offizielle Bibliothek hatte die Unterstützung für den Versand von Raw Packets aus der public API entfernt
- Gefunden wurde das GitHub-Projekt W5100MacRaw, das Packet-Senden und -Empfangen mit minimalen Schritten ausführt
- Der Vergleich dieses Quellcodes mit der eigenen Implementierung zeigte jedoch keine sofort entscheidenden Unterschiede
- Die Reihenfolge von Register-Read/Write war unterschiedlich
- Auf einer Seite gab es Reads/Writes, die auf der anderen fehlten
- Üblicherweise wird angenommen, dass Reihenfolgeabhängigkeiten im Datenblatt erwähnt werden
- Auch nachdem die Read/Write-Reihenfolge an die Referenzimplementierung angepasst wurde, zeigte Wireshark weiterhin Garbage-Pakete
Der eigentliche Bug, sichtbar gemacht durch ein kleines Tool
- Die nächste Strategie bestand darin, ein Tool zu schreiben, das die SPI-Capture-CSV aus Saleae Logic2 parst und wieder als Liste von Register-Reads/-Writes anzeigt
- Das Tool wurde in Python geschrieben und war insgesamt etwas über 200 Zeilen lang
- Der größte Teil bestand aus aus dem Datenblatt kopierten Register-Namen und -Adressen
- Die Erstellung dauerte etwa 1 Stunde
- Argument Parsing wurde ebenfalls eingebaut, um Eingabe und Ausgabe klar zu machen
- Von der Arduino-Referenzimplementierung und der eigenen Implementierung wurden jeweils SPI-Captures erstellt, als CSV exportiert, mit dem Tool verarbeitet und dann per diff verglichen
- Das Problem lag in der Hilfsfunktion
w5100_write16(u16 address, u16 value)- Viele Register des W5100 sind 16-bit-Werte
- Das Befehlsformat kann jeweils nur 8-bit schreiben, daher muss in High- und Low-Byte aufgeteilt werden
- Diese Funktion schrieb das zweite Byte nicht an
address + 1, sondern erneut an dieselbe Adresse
- Das Arduino-Referenzlog schrieb jeweils nach
S0_TX_WR0undS0_TX_WR1S0_TX_WR0 [0x0424] 0x00S0_TX_WR1 [0x0425] 0x3c
- Das eigene Log schrieb beides nach
S0_TX_WR0 [0x0424]S0_TX_WR0 [0x0424] 0x00S0_TX_WR0 [0x0424] 0x3c
- Dieses Register war der Socket 0 transmit write pointer
- Der W5100 erwartet den Ablauf, dieses 16-bit-Register der Reihe nach zu lesen
- Bytes in den TX-Speicher zu schreiben
- den neuen Write Pointer wieder zu schreiben
- und anschließend den Socket-Send-Command zu senden
- Weil das zweite Byte nicht geschrieben wurde und dennoch gesendet wurde, geriet der Chip in einen undefinierten, ungewöhnlichen Zustand; in Wireshark erschienen Pakete, die wie zufällige Ergebnisse wirkten
- Nach Korrektur der Funktion wurde das Testpaket in Wireshark korrekt angezeigt
Der Wert der Zeit, die man in Debugging-Tools steckt
- Die erste Ethernet-Paketübertragung selbst ist keine gewaltige Leistung, war im Projekt aber ein Moment, der klar als Erfolg gelten kann
- Bugs nachzuvollziehen macht in persönlichen Projekten Spaß, und Zeit in Tooling und die Erkundung des Debugging-Raums zu investieren, ist fast immer lohnend
- In manchen professionellen Umfeldern gilt Arbeit, die nicht direkt ein Deliverable erzeugt, tendenziell als Verschwendung
- Wenn Entwicklungsprozesse im JIRA-Stil zerlegt werden, wird Arbeit, die nicht unmittelbar eine Checkbox füllt, geringer bewertet
- Solange man ein System noch nicht ausreichend versteht, kann Tooling genauso wichtig sein wie Tests zu schreiben, manchmal sogar wichtiger
- Debugging ähnelt stark der Umsetzung der wissenschaftlichen Methode
- Daten sammeln
- Vorhersagen aufstellen
- Vorhersagen durch Experimente überprüfen
- Vorhersagen mit neuen Daten aktualisieren
- Dieses Projekt bewegt sich danach zu Problemen auf höheren Abstraktionsebenen weiter
- Missverständnisse bei RFCs
- Schreiben von Multitasking-Code
- Und neue Bugs, mit denen es weitergeht
1 Kommentare
Hacker-News-Meinungen
Die Fähigkeit, sich direkt kleine Tools bauen zu können, ist eine Superkraft und steckt oft im Kern dessen, was man gemeinhin einen 10x-Programmierer nennt.
Leider zeigt sich diese Fähigkeit meist still und leise an Stellen, die kaum sichtbar sind.
Aber für jemanden, der neugierig ist und über Sekundäreffekte oder Funktionen nachdenkt, die bald gebraucht werden, kann die Zusammenarbeit mit Entwicklern, die das nicht tun, ziemlich schmerzhaft sein.
Ich räume gerade ein Projekt auf, das jemand, mit dem ich kürzlich gearbeitet habe, umgesetzt hat — kein schlechter Mensch, aber er hat zu 100 % nur das implementiert, was im Ticket stand. Das neue Projekt sollte Probleme der bestehenden Software ersetzen, ließ aber aus denselben Gründen alle alten Probleme unverändert bestehen.
Allerdings riecht der Ausdruck 10x-Entwickler ein wenig nach Buzzword. Er erinnert mich an waghalsige Entwickler, die ohne Aufsicht „die Arbeit erledigen“, dabei hohe Kosten hinterlassen, alles siloartig aufbauen und es später wie ein Kartenhaus zusammenfällt, sobald jemand anderes etwas anfassen muss oder diese Person geht.
Idealerweise sollte jeder Explorationszeit bekommen, aber man braucht Manager, die das strikt schützen, auch wenn das obere Management sagt: „Das muss schneller fertig werden.“ Noch schwieriger wird es, wenn Manager anderer Teams fragen: „Darf dieses Team zusätzliche Leute einstellen, um die verlorene Zeit auszugleichen? Wir brauchen diese Kapazität auch.“
Am Ende braucht es Regelungen auf Organisationsebene, und selbst Google hat seine 20%-Zeit aufgegeben.
Die „unethische“ Lösung besteht darin, solche Zeit ein wenig in die Entwicklungsschätzungen einzupreisen.
Eigene Tools zu bauen ist in passenden Situationen nützlich, aber ich sehe produktive Entwickler auch als solche, die möglichst wenig Code schreiben und nutzen, was bereits existiert. Auf das Beispiel im Artikel bezogen: Niemand muss einen TCP-Stack selbst neu bauen; es gibt bereits hervorragende Implementierungen.
Trotzdem kann Selberbauen der beste Weg sein, um ein tiefes Verständnis zu erlangen, und tiefes Verständnis ist ein Bestandteil des mythischen 10x-Entwicklers. Ich würde allerdings nicht erwarten, dass der Arbeitgeber diesen Prozess bezahlt.
Wenn etwas normalerweise innerhalb eines Tages erledigt ist, mache ich es einfach. Es war noch nie falsch und auch nie verschwendet. Im schlimmsten Fall kopiere ich den Code später an eine andere Stelle.
Dann fing er an zu verlangen, dass auch Nichtprogrammierer sie nutzen können, und plötzlich wurden diese Tools zu einer viel größeren Aufgabe als erwartet.
Der Titel ist ziemlich vage; der Artikel ist der Auftakt zu einer Serie, in der von Grund auf ein TCP/IP- und Ethernet-Framing-Stack für Mikrocontroller gebaut wird.
Der Autor verwendet einen W5100-Chip, der TCP/IP selbst verarbeiten kann, aber auch unterstützt, vorgefertigte Ethernet-Frames zu übergeben. Präambel und CRC-Berechnung übernimmt allerdings der Chip.
Der Großteil des Artikels handelt davon, mit dem Chip selbst zu kommunizieren und ein Testpaket zu senden. Es sieht nach einem hartcodierten Paket aus, wird im Artikel aber nicht ausdrücklich gesagt.
Persönlich hatte ich gehofft, es ginge darum, Ethernet auf völlig abwegiger Hardware per Bit-Banging zu betreiben.
Einer der Tricks besteht darin, ein gängiges PHY-+MagJack-Board so umzubauen, dass der RP2040 das Taktsignal erzeugt. Dadurch sind die Signale synchronisiert und RMII muss nicht überabgetastet werden.
Wenn nur 10 Mb/s nötig sind, geht es auch mit noch schmutzigeren Methoden. Ich warte immer noch auf ein „modernes“ Glass-Terminal, das DVI/HDMI-Videoausgabe und Ethernet auf dem RP2040 kombiniert. Telnet allein würde schon reichen; SSH ist vermutlich zu viel verlangt.
Ich bin kürzlich beruflich auf etwas ungewöhnliche Weise in Richtung Ethernet-zentriertes FPGA-Engineering gewechselt.
Es war eine interessante Reise und führte schließlich dazu, dass ich selbst Hard-MAC-IP entworfen und Pakete über eine Custom-PHY-IP verschickt habe.
Ich kann diese Herausforderung sehr empfehlen, wenn man sie im „Hard Mode“ angehen möchte. Networking ist für Nutzer so stark abstrahiert, dass es wirklich wertvoll war zu verstehen, wie Ethernet-Karten, Modems und Switches die einzelnen Teile eines Pakets zusammensetzen und zerlegen und wie PHY/PCS Signale auf dem Link wiederherstellen.
Für Debugging und Verifikation haben wir eine mit Verilator erstellte Simulationsversion an ein Linux-TUN/TAP-Gerät anschließbar gemacht, sodass man sich direkt von der Entwickler-Maschine aus verbinden konnte, ohne die physische Hardware zu belegen. Das war besonders nützlich, weil es nur ein einziges Hardware-Exemplar gab.
Mein Networking-Wissen endet bei einem sehr grundlegenden Verständnis des OSI-Modells, aber ich würde gern tiefer in die Networking-Welt eintauchen.
Das ist das erste Mal, dass ich eine Neuinterpretation von MOSI/MISO gesehen habe: Statt master out/slave in heißt es main out/subordinate in.
So kann man die Pins weiterhin MOSI/MISO nennen, daher werde ich das wohl auch verwenden. Die Alternative COPI/CIPO (controller out/peripheral in) ist bei mir nie richtig hängen geblieben.
Ich wundere mich, warum der Autor ein W5100-Ethernet-Shield und einen STM32F401 verwendet. Genauso einfach könnte man ein STM32F407-Board nehmen; das hat einen integrierten Ethernet-MAC und lässt sich zusammen mit einem günstigen Ethernet-PHY-Board entwickeln.
Es gibt auch viele Beispielprojekte für Ethernet, und die Entwicklung ist genauso einfach wie beim STM32F401.
Außerdem halte ich die Erklärung, „wegen der Komplexität der Ethernet-Signalverarbeitung verwendet man normalerweise einen dedizierten ASIC“, im Kontext von Mikrocontrollern größtenteils nicht für zutreffend. In vielen Fällen ist Ethernet als integriertes Peripheriegerät im Mikrocontroller enthalten, etwa beim STM32F407 oder ESP32.
Es ist definitiv sinnvoller, einen Chip mit integrierter Ethernet-Peripherie zu verwenden. Allerdings tauscht man damit auch die Komplexität der W5100-Konfiguration gegen die Komplexität der ST-Peripheriekonfiguration ein.
Der Networking-Code abstrahiert den tatsächlichen Chip bereits hinter einem Treiber-Interface (etwa in der Form read/write/ioctl), daher sollte das Portieren ziemlich einfach sein.
In dieser Serie werde ich mir den STM32F407 ansehen.
Ethernet arbeitet nicht mit Paketen, sondern mit Frames.
Pakete sind ein IP-Konzept.
Allerdings stammt RFC 791 aus einer Zeit, in der das darunterliegende L2-Netzwerk mit hoher Wahrscheinlichkeit das ARPAnet mit 128-Byte-Paketen war.
Heute sollte diese Unterscheidung praktisch bedeutungslos sein. Wenn man sich darauf verlässt, große IP-Datagramme per IP-Fragmentierung über mehrere L2-Pakete zu verschicken, läuft etwas falsch. IPv6 unterstützt nicht einmal Fragmentierung innerhalb des Netzwerks.
In der Praxis ist die Unterscheidung mit der Zeit fast verschwunden. Normalerweise enthält ein Ethernet-Frame ein IP-Datagramm, und alle nennen das einfach Paket.
https://en.wikipedia.org/wiki/Ethernet_frame
Wenn man auf einem Mikrocontroller kabelgebundenes Ethernet ausprobieren möchte: Einige der größeren STM32-Nucleo-Boards haben 100-Mbps-Ethernet integriert und sind mit etwa 25 Dollar ziemlich günstig.
Die Bewertungen der STM32Cube-Software sind gemischt, aber sie erzeugt ein funktionierendes Beispiel für Ethernet-Kommunikation.
https://www.st.com/en/evaluation-tools/nucleo-f439zi.html
MQTT, HTTP-Client und -Server usw. werden mitgeliefert.
Als ich Cube-MX zuletzt benutzt habe, war die Erfahrung insgesamt äußerst unangenehm. Wenn ich wieder STM32 verwenden würde, würde ich wahrscheinlich stm32-hal oder libopencm3 bevorzugen. Das Tool selbst und der generierte Code hatten alle möglichen üblen Bugs und Grenzfälle, sodass ich Tage mit Debugging verbrachte. Vielleicht ist es inzwischen besser.
https://github.com/egnor/wt32-eth01
https://liliputing.com/waveshare-esp32-p4-nano-is-a-tiny-ris...
Ich habe gerade gesehen, dass es auch günstigere Module gibt. Dinge wie der WT32-ETH01 könnten weniger leistungsfähig sein.
Wenn man unter Linux selbst einen Netzwerk-Stack schreiben möchte, kann man
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))verwenden und auf einer Abstraktionsebene arbeiten, die sich nicht stark von der in diesem Artikel unterscheidet.SOCK_RAW-Pakete werden zwischen Gerätetreiber und Anwendung übertragen, ohne die Paketdaten zu verändern. Beim Empfangen wird die Adresse geparst und in der standardmäßigen Adressstruktursockaddr_llübergeben.Beim Senden muss der vom Benutzer bereitgestellte Buffer den Header der physikalischen Schicht enthalten, und das Paket landet unverändert in der Netzwerk-Treiber-Queue des Interfaces, das durch die Zieladresse angegeben ist.
https://man7.org/linux/man-pages/man7/packet.7.html
Das fügt dem Netzwerk-Stack ein virtuelles Interface hinzu, ähnlich wie ein VPN-Interface. Packet-Sockets erlauben dagegen die direkte Kommunikation mit einem realen Interface.
Genau genommen sollte man wohl Ethernet-Frame sagen.
Gut gemacht. Ich habe gerade die letzten 16 Arbeitsstunden mit der Gegenrichtung verbracht: einer Implementierung, die Ethernet II (einschließlich VLAN), IPv4+6 und UDP bis hin zu Automotive-IP-Protokollen parst.
Zweck ist es, einen proprietären Bus-Capture-Stream zu verstehen; dieser Stream transportiert Ethernet-Frames, die wiederum in Ethernet-Frames verschachtelt sind usw., und ich empfange sie über Raw Sockets.
Für solche Arbeiten sind Wireshark und ChatGPT wirklich unschätzbar wertvoll.