- Comme première étape pour construire directement une pile TCP/IP sur un microcontrôleur, l’auteur a relié une carte STM32F401 Nucleo à un shield Wiznet W5100 pour tenter d’envoyer une trame Ethernet
- Sans utiliser les fonctions TCP/IP matérielles du W5100, il s’est limité au mode MAC Raw, en ne confiant à la puce que le traitement de bas niveau nécessaire à la transmission de la trame
- Le premier problème venait du fait que le câblage SPI du shield Arduino Ethernet ne correspondait pas à celui du Nucleo ; après vérification du routage via le connecteur ICSP, le problème a été résolu avec un recâblage de la carte
- Ensuite, entre une réponse MISO anormale semblant liée à un problème de timing du chip select et des paquets parasites dans Wireshark, l’analyseur logique et la comparaison avec une implémentation de référence ont été les principaux outils de débogage
- La cause finale était un bug dans
w5100_write16()qui réécrivait le second octet à la même adresse ; grâce à un outil d’analyse de CSV de captures SPI, l’auteur est finalement parvenu à transmettre correctement un paquet
Point de départ pour construire sa propre pile TCP/IP
- L’objectif est de lancer une série « Networking from scratch » visant à implémenter une pile TCP/IP depuis les fondations sur un microcontrôleur
- Le résultat visible de cette étape est l’envoi d’un premier paquet Ethernet, mais le véritable sujet est surtout le processus de traque des bugs entre le matériel et le pilote
- La carte utilisée est une carte de développement Nucleo basée sur un STM32F401
- ARM Cortex-M4
- fonctionnement jusqu’à 84 MHz
- 96 KiB de RAM
- mémoire jugée suffisante pour conserver plusieurs paquets
Le rôle d’Ethernet et du W5100
- Ethernet n’est pas simplement un port ou un format de trame, mais une famille de technologies et de standards qui couvre le matériel de couche physique, la signalisation, la gestion des collisions sur le bus et jusqu’à la mise en forme des trames
- Comme le traitement du signal Ethernet est complexe, on utilise en général un ASIC dédié qui reçoit les données au niveau trame et gère le traitement électrique sur le câble
- Le projet utilise un shield Arduino Ethernet intégrant une puce Wiznet W5100
- La carte utilisée était une copie d’entrée de gamme, qui a nécessité des modifications pour fonctionner correctement
- Le W5100 est une puce qui intègre une pile TCP/IP matérielle à son ASIC Ethernet
- elle fournit 4 « sockets »
- configurables aux niveaux TCP, UDP, IP et « MAC Raw »
- Comme le but est d’implémenter soi-même la pile TCP/IP, les fonctions TCP/IP du W5100 ne sont pas utilisées ; un seul socket est exploité en mode MAC Raw
- l’utilisateur fournit une trame Ethernet
- le W5100 effectue la transmission réelle
- le préambule et le start-of-frame marker sont des éléments de niveau électrique pris en charge par la puce
- le CRC 32 bits est aussi calculé par le W5100
Problème 1 : le signal SPI n’arrive pas jusqu’au W5100
- L’échange de données avec le W5100 se fait en SPI
- MOSI : sortie du microcontrôleur principal
- MISO : sortie du W5100
- clock : horloge de référence pour les données
- chip select : signal indiquant qu’une communication est en cours avec la puce esclave
- La documentation du W5100 définit au-dessus du SPI un protocole de commande sur 4 octets
- 1 octet d’opération
- 2 octets d’adresse 16 bits big-endian
- 1 octet de valeur
- L’opération est soit une écriture
0xf0, soit une lecture0x0f- les autres valeurs ne sont pas valides et doivent être ignorées
- Le W5100 renvoie une valeur connue sur MISO à chaque octet cadencé, ce qui permet de repérer facilement les anomalies de communication
- pour une commande de lecture, le 4e octet correspond à la valeur lue à l’adresse indiquée
- Au départ, malgré l’envoi des commandes sur MOSI, des valeurs parasites apparaissaient sur MISO
- La cause venait du fait que le shield Arduino Ethernet routait les signaux SPI non pas vers le connecteur standard Arduino, mais vers le connecteur 6 broches ICSP
- sur une carte Arduino officielle, les signaux SPI du connecteur ICSP et de l’en-tête standard sont reliés en interne, donc cela ne pose pas de problème
- la carte Nucleo n’ayant pas de connecteur ICSP, les signaux SPI envoyés n’atteignaient jamais le W5100
- En mesurant la résistance entre les rails d’alimentation et les signaux SPI au multimètre, l’auteur a confirmé le problème de connexion
- une résistance infinie indique qu’une liaison est coupée
- Les signaux entre le Nucleo et le W5100 ont finalement été câblés directement avec de la soudure et du fil de cuivre émaillé
Problème 2 : timing du chip select et réponse MISO anormale
- Une fois les signaux SPI réellement connectés, il est devenu possible de communiquer avec le W5100, puis d’implémenter l’étape suivante
- configuration du W5100 pour la transmission Ethernet brute
- configuration de l’adresse MAC
- configuration des segments mémoire TX/RX
- écriture d’une trame Ethernet de test en mémoire TX puis déclenchement de l’envoi
- Le Nucleo et le shield ont été reliés à un ordinateur portable avec un câble CAT5 et Wireshark lancé, mais aucun paquet n’apparaissait
- Les problèmes de bas niveau sont difficiles à suivre : il n’y a ni message d’erreur ni stack trace, seulement « on a agité des électrons, mais il ne se passe pas ce qu’on attend »
- Le débogage s’est fait avec un analyseur logique
- il échantillonne les transitions high/low sur plusieurs canaux de signaux numériques
- le logiciel peut interpréter des ensembles de signaux comme un bus SPI et afficher les octets des transactions
- les captures peuvent être exportées dans un format structuré comme le CSV
- L’appareil utilisé est un Saleae Logic 8
- des analyseurs bon marché autour de 10 € peuvent aussi fonctionner avec Saleae Logic2, mais avec des compromis sur la fiabilité et la régularité du fonctionnement
- La première commande SPI semblait correcte
0xf0 0x00 0x00 0x80- elle positionne le bit de poids fort du registre Mode à l’adresse
0x0000pour déclencher un software reset - MISO répondait correctement avec
0x00à0x03
- La commande de lecture suivante montrait en revanche une réponse anormale
- MOSI :
0x0f 0x00 0x00 0x00 - MISO :
0x03 0xff 0xff 0xff
- MOSI :
- Le fait que
0x03corresponde à la dernière valeur de la transaction précédente a conduit à soupçonner un problème d’état interne ou de timing du W5100 - La fréquence SPI utilisée était pourtant largement inférieure au maximum d’environ 14 MHz indiqué par la documentation, ce qui a écarté cette piste
- L’hypothèse retenue a été que le chip select repassait à l’état haut trop vite, mettant la puce dans un mauvais état ; un délai de quelques microsecondes a donc été ajouté avant ce changement d’état
- ensuite, la réponse MISO est redevenue normale
- relire les valeurs de configuration donnait aussi des résultats cohérents
- La cause exacte de ce problème n’est toujours pas totalement élucidée
- les contraintes liées au chip select dans le diagramme de timing SPI de la page 66 de la documentation étaient pourtant respectées
- il serait possible de réexaminer les conditions limites exactes, mais la priorité a été donnée à la poursuite du projet
Problème 3 : paquets parasites dans Wireshark
- En relançant Wireshark, des paquets apparaissaient bien, mais pas ceux qui étaient attendus
- Au moment où le microcontrôleur envoyait la commande de transmission, un paquet Ethernet brut bien plus grand que celui prévu apparaissait, rempli de données parasites
- L’auteur a alors abandonné l’idée de tout implémenter uniquement à partir de la documentation et des spécifications, pour comparer à ce stade avec une implémentation de référence déjà fonctionnelle
- Arduino est pratique pour produire rapidement une implémentation de référence
- avec Arduino, sa bibliothèque et environ 5 lignes de code, il est possible de valider un comportement complexe
- Il a été plus long de trouver une bibliothèque pour envoyer des paquets Ethernet bruts
- la plupart des utilisateurs d’Arduino ne cherchent pas à implémenter eux-mêmes les fonctions réseau depuis zéro
- la bibliothèque officielle a supprimé le support de l’envoi de paquets bruts de son API publique
- Un projet GitHub minimaliste, W5100MacRaw, a finalement été trouvé pour l’envoi et la réception de paquets
- La comparaison entre cette source et l’implémentation personnelle n’a pas révélé immédiatement de différence décisive
- l’ordre des lectures/écritures de registres différait
- certaines lectures/écritures n’existaient que d’un côté
- en général, si le fonctionnement dépend de l’ordre, cela est explicitement indiqué dans la documentation
- Même en alignant l’ordre des lectures/écritures sur celui de l’implémentation de référence, Wireshark montrait toujours des paquets parasites
Le vrai bug révélé par un petit outil
- La stratégie suivante a consisté à écrire un outil capable d’analyser le CSV d’une capture SPI de Saleae Logic2 pour réafficher la liste des lectures/écritures de registres
- L’outil a été écrit en Python et fait un peu plus de 200 lignes au total
- l’essentiel du code correspond à la liste des noms et adresses de registres copiés depuis la documentation
- son écriture a pris environ une heure
- un parsing des arguments a aussi été ajouté pour clarifier les entrées et sorties
- Des captures SPI ont été réalisées à la fois sur l’implémentation de référence Arduino et sur l’implémentation personnelle, exportées en CSV, traitées avec l’outil puis comparées avec un diff
- Le problème venait d’une fonction utilitaire
w5100_write16(u16 address, u16 value)- de nombreux registres du W5100 utilisent des valeurs 16 bits
- comme le format de commande ne permet d’écrire que 8 bits à la fois, il faut séparer la valeur en octet de poids fort et octet de poids faible
- cette fonction réécrivait le second octet à la même adresse au lieu de l’écrire à
address + 1
- Dans les logs de l’implémentation de référence Arduino, l’écriture se faisait bien dans
S0_TX_WR0puisS0_TX_WR1S0_TX_WR0 [0x0424] 0x00S0_TX_WR1 [0x0425] 0x3c
- Dans les logs de l’auteur, les deux écritures allaient dans
S0_TX_WR0 [0x0424]S0_TX_WR0 [0x0424] 0x00S0_TX_WR0 [0x0424] 0x3c
- Ce registre est le pointeur d’écriture de transmission du socket 0
- le W5100 lit ce registre 16 bits dans l’ordre
- écrit les octets dans la mémoire TX
- réécrit ensuite le nouveau pointeur d’écriture
- puis attend l’envoi de la commande de transmission du socket
- En lançant l’envoi sans avoir écrit correctement le second octet, la puce entrait dans un état anormal non défini, et Wireshark affichait des paquets au comportement apparemment aléatoire
- Une fois la fonction corrigée, le paquet de test est apparu correctement dans Wireshark
La valeur du temps passé à fabriquer des outils de débogage
- L’envoi du tout premier paquet Ethernet n’est pas en soi un exploit immense, mais dans le cadre du projet, cela constituait clairement une réussite
- Revenir sur ses bugs fait partie du plaisir des projets personnels, et passer du temps à fabriquer des outils et explorer l’espace du débogage est presque toujours rentable
- Dans certains environnements professionnels, le travail qui ne produit pas directement un livrable est parfois considéré comme du gaspillage
- à mesure que le processus de développement se découpe à la manière de JIRA, les tâches qui ne remplissent pas directement une case à cocher sont moins valorisées
- quand on ne comprend pas encore suffisamment le système, écrire des outils peut être aussi important que rédiger des tests, voire parfois plus important
- Le débogage ressemble beaucoup à une mise en œuvre de la méthode scientifique
- on collecte des données
- on formule des prédictions
- on vérifie ces prédictions par l’expérimentation
- on met à jour les prédictions avec les nouvelles données
- Le projet se déplace ensuite vers des problèmes de niveau d’abstraction plus élevé
- mauvaise compréhension de RFC
- écriture de code multitâche
- et progression vers de nouveaux bugs
1 commentaires
Avis sur Hacker News
La capacité à créer soi-même de petits outils est un super-pouvoir, et se trouve souvent au cœur de ce qu’on appelle un programmeur 10x.
Malheureusement, ce genre de compétence s’exerce généralement discrètement, dans des endroits peu visibles.
Mais pour quelqu’un de curieux, qui pense aux effets de second ordre ou aux fonctionnalités qui seront bientôt nécessaires, travailler avec des développeurs qui ne le font pas peut être assez pénible.
Récemment, j’ai travaillé avec quelqu’un qui n’était pas une mauvaise personne, mais je suis en train de remettre en ordre un projet où il avait implémenté à 100 % uniquement ce que le ticket demandait. Le nouveau projet était censé remplacer les problèmes du logiciel existant, mais pour les mêmes raisons, tous les problèmes existants étaient restés exactement tels quels.
Cela dit, l’expression développeur 10x sent un peu le buzzword. Elle me fait penser à des développeurs téméraires qui “font le boulot” sans supervision en laissant derrière eux de gros coûts, avec tout qui finit en silos et qui s’effondre ensuite comme un château de cartes dès que quelqu’un d’autre y touche ou que la personne s’en va.
Idéalement, tout le monde devrait disposer de temps d’exploration, mais il faut aussi un manager prêt à le défendre strictement quand la direction dit qu’“il faut aller plus vite”. Cela devient encore plus difficile si le manager d’une autre équipe dit : “Est-ce que cette équipe peut embaucher davantage pour compenser le temps perdu ? Nous aussi, nous avons besoin de ces effectifs.”
Au final, il faut un dispositif à l’échelle de l’organisation, et même Google a abandonné le temps de 20 %.
La solution “non éthique” consiste à inclure discrètement un peu de ce temps dans les estimations de développement.
Créer ses propres outils est utile quand le contexte s’y prête, mais je pense aussi qu’un développeur productif écrit le moins de code possible et s’appuie sur ce qui existe déjà. Pour reprendre l’exemple de l’article, personne n’a besoin de recréer une pile TCP à la main : il existe déjà d’excellentes implémentations.
Malgré tout, le faire soi-même peut être le meilleur moyen d’acquérir une compréhension profonde, et cette compréhension profonde fait partie des ingrédients du mythique développeur 10x. Je ne m’attendrais toutefois pas à ce qu’un employeur paie pour ce processus.
En général, si ça prend moins d’une journée, je le fais. Je ne me suis jamais trompé, et cela n’a jamais été du gaspillage. Dans le pire des cas, je finis plus tard par copier-coller ce code ailleurs.
Puis il a commencé à me demander de le rendre utilisable par des non-programmeurs, et tout à coup ces outils sont devenus une tâche bien plus importante que prévu.
Le titre est assez ambigu : l’article marque le début d’une série où l’auteur construit à partir de zéro une pile TCP/IP et de trames Ethernet pour microcontrôleur.
L’auteur utilise une puce W5100 capable de gérer TCP/IP elle-même, mais qui prend aussi en charge l’envoi de trames Ethernet préconstruites. En revanche, le préambule et le calcul du CRC sont pris en charge par la puce.
La majeure partie de l’article porte sur la communication avec la puce elle-même et l’envoi d’un paquet de test. Cela ressemble à un paquet codé en dur, même si l’article ne le précise pas explicitement.
Personnellement, j’espérais un article sur du bit banging Ethernet sur du matériel complètement improbable.
L’une des astuces consiste à modifier une carte PHY + MagJack courante pour que le RP2040 génère le signal d’horloge. Ainsi, le signal est synchronisé et il n’est pas nécessaire de suréchantillonner le RMII.
Si 10 Mb/s suffisent, on peut même employer des méthodes plus bricolées. J’attends toujours qu’apparaisse un terminal “moderne” en verre combinant sortie vidéo DVI/HDMI et Ethernet sur RP2040. Telnet suffirait, SSH serait probablement trop ambitieux.
J’ai récemment effectué une reconversion un peu atypique vers de l’ingénierie FPGA centrée sur Ethernet.
Le parcours a été passionnant, et j’ai fini par concevoir moi-même une IP Hard MAC et envoyer des paquets via une IP PHY personnalisée.
Je le recommande vivement à quiconque veut tenter ce défi en “mode difficile”. Le réseau est tellement abstrait pour l’utilisateur qu’il a été vraiment précieux de comprendre comment les cartes Ethernet, modems et switches assemblent et désassemblent chaque partie des paquets, et comment la PHY/PCS récupère le signal sur le lien.
Pour le débogage et la vérification, nous avions rendu possible la connexion d’une version simulée avec Verilator à un périphérique TUN/TAP Linux, ce qui permettait de s’y connecter directement depuis la machine d’un développeur sans monopoliser le matériel physique. C’était particulièrement utile, car il n’y avait qu’un seul exemplaire du matériel.
Mes connaissances réseau s’arrêtent à une compréhension très basique du modèle OSI, mais j’aimerais explorer cet univers en profondeur.
C’est la première fois que je vois cette réinterprétation de MOSI/MISO : au lieu de master out/slave in, on dit main out/subordinate in.
Cela permet de continuer à appeler les broches MOSI/MISO, donc je pense que je l’utiliserai aussi. L’alternative COPI/CIPO (controller out/peripheral in) ne m’est jamais vraiment restée en tête.
Je trouve curieux que l’auteur utilise un shield Ethernet W5100 et un STM32F401. En prenant tout aussi simplement une carte STM32F407, on dispose d’un MAC Ethernet intégré, et on peut la combiner avec une carte PHY Ethernet peu coûteuse pour développer
Il existe aussi beaucoup de projets Ethernet d’exemple, et c’est aussi facile à développer qu’avec un STM32F401
Par ailleurs, l’explication selon laquelle « à cause de la complexité du traitement des signaux Ethernet, on utilise généralement un ASIC dédié » me semble globalement inexacte dans le contexte des microcontrôleurs. Dans beaucoup de cas, la fonction Ethernet est intégrée comme périphérique dans le microcontrôleur, comme sur le STM32F407 ou l’ESP32
Utiliser une puce avec un périphérique Ethernet intégré serait clairement plus logique. Cela dit, cela revient aussi à échanger la complexité de configuration du W5100 contre celle de la configuration des périphériques ST
Le code réseau abstrait déjà la puce réelle derrière une interface de pilote (du type read/write/ioctl), donc le portage devrait être assez simple
Dans cette série, j’examinerai le STM32F407
Ethernet ne manipule pas des paquets, mais des trames
Le paquet est un concept IP
Cela dit, la RFC 791 date d’une époque où le réseau L2 sous-jacent avait de fortes chances d’être ARPAnet, avec ses paquets de 128 octets
Aujourd’hui, cette distinction devrait être pratiquement sans importance. Si l’on compte sur la fragmentation IP pour envoyer un gros datagramme IP sous forme de plusieurs paquets L2, c’est qu’il y a un problème. IPv6 ne prend même pas en charge la fragmentation au sein du réseau
Dans la pratique, la distinction s’est presque effacée avec le temps. En général, une trame Ethernet contient un datagramme IP, et tout le monde appelle simplement ça un paquet
https://en.wikipedia.org/wiki/Ethernet_frame
Si vous voulez essayer l’Ethernet filaire sur microcontrôleur, certaines des plus grosses cartes STM32 Nucleo intègrent de l’Ethernet 100 Mbps et restent assez abordables, autour de 25 dollars
Les avis sur le logiciel STM32Cube sont partagés, mais il génère des exemples de communication Ethernet fonctionnels
https://www.st.com/en/evaluation-tools/nucleo-f439zi.html
MQTT, client et serveur HTTP, etc. sont fournis avec
La dernière fois que j’ai utilisé Cube-MX, l’expérience globale a été très désagréable. Si je devais reprendre du STM32, je préférerais sans doute stm32-hal ou libopencm3. Il y avait toutes sortes de mauvais bugs et de cas limites dans l’outil lui-même et dans le code généré, et j’ai passé des jours à déboguer. C’est peut-être mieux maintenant
https://github.com/egnor/wt32-eth01
https://liliputing.com/waveshare-esp32-p4-nano-is-a-tiny-ris...
Je viens de voir qu’il existe aussi des modules moins chers. Les modèles comme le WT32-ETH01 peuvent être moins performants
Si vous voulez écrire vous-même une pile réseau sous Linux, vous pouvez utiliser
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))et travailler à un niveau d’abstraction qui n’est pas très différent de celui de cet articleLes paquets
SOCK_RAWsont échangés avec le pilote de périphérique sans modification des données du paquet. À la réception, l’adresse est analysée et transmise dans une structure d’adresse standardsockaddr_llÀ l’émission, le tampon fourni par l’utilisateur doit contenir l’en-tête de couche physique, et le paquet est placé tel quel dans la file du pilote réseau de l’interface indiquée par l’adresse de destination
https://man7.org/linux/man-pages/man7/packet.7.html
Cela ajoute une interface virtuelle à la pile réseau, comme une interface VPN. À l’inverse, les sockets paquet permettent de communiquer directement avec une interface réelle
Plus exactement, il me semble qu’il faudrait dire trame Ethernet
Bien réalisé. Je viens de passer les 16 dernières heures de travail sur l’opération inverse : une implémentation qui analyse Ethernet II (VLAN inclus), IPv4+6 et UDP jusqu’au protocole IP automobile
Le but est de comprendre un flux de capture de bus propriétaire, qui transporte des trames Ethernet imbriquées dans des trames Ethernet, entre autres, et que je reçois via des sockets bruts
Pour ce genre de travail, Wireshark et ChatGPT sont vraiment précieux