- Como primer paso para construir directamente una pila TCP/IP en un microcontrolador, se conectó una STM32F401 Nucleo con un shield Wiznet W5100 para intentar enviar tramas Ethernet
- Sin usar las funciones de TCP/IP por hardware del W5100, se utilizó solo el modo MAC Raw, dejando al chip únicamente el procesamiento de bajo nivel necesario para transmitir tramas
- El primer problema comenzó porque el cableado SPI del Arduino Ethernet shield no coincidía con la Nucleo, y se resolvió verificando el enrutamiento del header ICSP y haciendo un recableado correctivo en la placa
- Después, entre una respuesta anómala de MISO que parecía un problema de timing de chip select y paquetes basura en Wireshark, un analizador lógico y la comparación con una implementación de referencia se volvieron las herramientas clave de depuración
- La causa final fue un bug en
w5100_write16()que volvía a escribir el segundo byte en la misma dirección, y gracias a crear una herramienta para analizar CSV de capturas SPI se logró finalmente transmitir un paquete correcto
Punto de partida para construir una pila TCP/IP propia
- El objetivo es iniciar una serie de “Networking from scratch” para implementar una pila TCP/IP desde cero sobre un microcontrolador
- El resultado visible de esta etapa es el envío del primer paquete Ethernet, pero el verdadero foco está en el proceso de rastrear bugs que aparecieron entre el hardware y el driver
- La placa usada fue una Nucleo basada en STM32F401
- ARM Cortex-M4
- Hasta 84MHz de operación
- 96KiB de RAM
- Se consideró que tenía memoria suficiente para almacenar varios paquetes
Ethernet y el papel del W5100
- Ethernet no es solo un puerto o un formato de trama, sino una familia de tecnologías y estándares que incluye hardware de capa física, métodos de señalización, manejo de colisiones en el bus y disposición de tramas
- Como el procesamiento de señales Ethernet es complejo, normalmente un ASIC dedicado recibe datos a nivel de trama y se encarga del tratamiento de las señales eléctricas del cable
- En el proyecto se usó un Arduino Ethernet shield con el chip Wiznet W5100
- La placa usada era una copia económica, así que hizo falta modificarla para que funcionara correctamente
- El W5100 es un chip que integra una pila TCP/IP por hardware dentro del ASIC Ethernet
- Proporciona 4 “sockets”
- Puede configurarse a nivel TCP, UDP, IP o “MAC Raw”
- Como el objetivo era implementar la pila TCP/IP directamente, no se usaron las funciones TCP/IP del W5100 y solo se utilizó un socket en modo MAC Raw
- El usuario entrega una trama Ethernet
- El W5100 realiza la transmisión real
- El preámbulo y el start-of-frame marker son elementos del nivel eléctrico, así que el chip se encarga de ellos
- El CRC de 32 bits también lo calcula el W5100
Problema 1: la señal SPI no llegaba al W5100
- El intercambio de datos con el W5100 se hace por SPI
- MOSI: salida del chip principal, el microcontrolador
- MISO: salida del W5100
- clock: reloj de referencia de los datos
- chip select: señal que indica que se está comunicando con el chip esclavo
- El datasheet del W5100 define sobre SPI un protocolo de comando de 4 bytes
- 1 byte de operación
- 2 bytes de dirección de 16 bits big-endian
- 1 byte de valor
- La operación puede ser escritura
0xf0o lectura0x0f- Otros valores no son válidos y deberían ignorarse
- El W5100 devuelve valores conocidos por MISO a medida que hace clock out de los bytes, así que es fácil detectar problemas de comunicación
- En un comando de lectura, el cuarto byte pasa a ser el valor leído desde la dirección indicada
- Al principio, aunque se enviaban comandos por MOSI, en MISO aparecían valores basura
- La causa era que el Arduino Ethernet shield estaba diseñado para enrutar las señales SPI no por el header estándar de Arduino sino por el header ICSP de 6 pines
- En una placa Arduino oficial, las señales SPI del ICSP y del header estándar están conectadas internamente, así que no hay problema
- La Nucleo no tiene header ICSP, así que la señal SPI enviada no llegaba al W5100
- Midiendo con un multímetro la resistencia entre los rieles de alimentación y las señales SPI se confirmó el problema de conexión
- Si aparece resistencia infinita, se sabe que el cableado está cortado
- Se conectaron directamente las señales entre la Nucleo y el W5100 con soldadura y alambre de cobre esmaltado
Problema 2: timing de chip select y respuesta anómala de MISO
- Una vez conectadas de verdad las señales SPI, ya fue posible comunicarse con el W5100 y avanzar hasta implementar lo siguiente
- Configuración del W5100 para transmisión raw Ethernet
- Configuración de la dirección MAC
- Configuración de los segmentos de memoria TX/RX
- Escritura de una trama Ethernet de prueba en la memoria TX y disparo de la transmisión
- Se conectó la Nucleo y el shield a una laptop con un cable CAT5 y se ejecutó Wireshark, pero no aparecía ningún paquete
- Los problemas de bajo nivel son difíciles de rastrear porque no hay mensajes de error ni stack traces; simplemente “se agitaron electrones pero no pasó lo esperado”
- Para depurar se usó un analizador lógico
- Muestrea los cambios high/low de señales digitales en varios canales
- El software interpreta grupos de señales como un bus SPI y muestra los bytes de cada transacción
- Puede exportar en formatos estructurados como CSV
- El equipo usado fue un Saleae Logic 8
- También hay analizadores baratos de alrededor de €10 que pueden integrarse con Saleae Logic2, pero con compromisos en funcionamiento correcto y consistencia
- El primer comando SPI parecía correcto
0xf0 0x00 0x00 0x80- Activa el bit más alto del Mode Register en la dirección
0x0000para disparar un software reset - MISO respondió correctamente de
0x00a0x03
- En el siguiente comando de lectura apareció una respuesta anómala
- MOSI:
0x0f 0x00 0x00 0x00 - MISO:
0x03 0xff 0xff 0xff
- MOSI:
- Como
0x03era el último valor de la transacción anterior, se sospechó de un problema de estado interno o timing dentro del W5100 - El reloj SPI se estaba usando muy por debajo del máximo de aproximadamente 14MHz indicado en el datasheet, así que no se consideró la causa
- En cambio, se asumió que chip select estaba volviendo a high demasiado rápido y metía al chip en un mal estado, así que se agregó una espera de algunos microsegundos antes de cambiar el estado
- Después de eso, la respuesta MISO volvió a la normalidad
- Al releer los valores de configuración también aparecieron valores razonables
- La causa exacta de este problema todavía no se entiende del todo
- Se estaban cumpliendo las restricciones relacionadas con chip select del diagrama de timing SPI de la página 66 del datasheet
- Se podrían investigar de nuevo las condiciones límite exactas, pero por ahora se priorizó avanzar con el proyecto
Problema 3: paquetes basura en Wireshark
- Al volver a ejecutar Wireshark, aparecieron paquetes, pero no eran los paquetes esperados
- En el momento en que el microcontrolador daba la orden de transmitir, aparecía un paquete Ethernet raw mucho más grande que el original y lleno de datos basura
- Se abandonó la idea de implementar esta parte solo a partir del datasheet y la especificación, y en esta etapa se decidió comparar con una implementación ya comprobada
- Arduino es útil para crear rápidamente una implementación de referencia
- Con Arduino, sus librerías y unas 5 líneas de código se puede verificar un comportamiento complejo
- Encontrar una librería para transmitir paquetes Ethernet raw tomó más tiempo
- La mayoría de quienes usan Arduino no intentan implementar funciones de red desde cero
- La librería oficial eliminó de su API pública el soporte para envío de paquetes raw
- Se encontró el proyecto de GitHub W5100MacRaw para enviar y recibir paquetes en el nivel mínimo
- Se comparó ese código con la implementación propia, pero las diferencias visibles de inmediato no parecían decisivas
- El orden de lectura/escritura de registros era distinto
- Había lecturas/escrituras presentes solo en uno de los lados
- Normalmente, si hubiera dependencia del orden, se esperaría que el datasheet lo indicara
- Incluso igualando el orden de lectura/escritura con la implementación de referencia, Wireshark seguía mostrando paquetes basura
El bug real revelado por una herramienta pequeña
- La siguiente estrategia fue escribir una herramienta para volver a mostrar como lista de lecturas/escrituras de registros los CSV de capturas SPI de Saleae Logic2
- La herramienta se hizo en Python y tenía poco más de 200 líneas en total
- La mayor parte eran nombres y direcciones de registros copiados del datasheet
- Llevarla a cabo tomó alrededor de 1 hora
- También se agregó parseo de argumentos para dejar clara la entrada y la salida
- Se capturó SPI tanto de la implementación de referencia en Arduino como de la implementación propia, se exportó a CSV y luego se procesó con la herramienta para hacer un diff
- El problema estaba en la función de conveniencia
w5100_write16(u16 address, u16 value)- Muchos registros del W5100 son valores de 16 bits
- El formato del comando solo permite escribir 8 bits por vez, así que hay que dividir en byte alto y byte bajo
- Esta función no escribía el segundo byte en
address + 1, sino que lo volvía a escribir en la misma dirección
- El log de referencia de Arduino escribía respectivamente en
S0_TX_WR0yS0_TX_WR1S0_TX_WR0 [0x0424] 0x00S0_TX_WR1 [0x0425] 0x3c
- En el log propio, ambos se escribían en
S0_TX_WR0 [0x0424]S0_TX_WR0 [0x0424] 0x00S0_TX_WR0 [0x0424] 0x3c
- Ese registro era el puntero de escritura de transmisión del Socket 0
- El W5100 espera leer ese registro de 16 bits en orden
- escribir bytes en la memoria TX
- volver a escribir el nuevo write pointer
- y luego enviar el comando de envío del socket
- Al ejecutar send sin haber escrito el segundo byte, el chip entraba en un estado anómalo no definido y en Wireshark aparecían paquetes con aspecto aleatorio
- Al corregir la función, el paquete de prueba se mostró correctamente en Wireshark
El valor de dedicar tiempo a crear herramientas de depuración
- Enviar el primer paquete Ethernet no es un logro gigantesco por sí mismo, pero dentro del proyecto sí podía asumirse como un éxito claro
- Repasar bugs es divertido en un proyecto personal, y dedicar tiempo a crear herramientas y explorar el espacio de depuración casi siempre vale la pena
- En algunos entornos profesionales existe la tendencia a ver como desperdicio el trabajo que no produce un deliverable directo
- Cuando el proceso de desarrollo se fragmenta al estilo JIRA, el trabajo que no marca casillas de forma directa tiende a ser subvalorado
- Cuando todavía no se entiende lo suficiente el sistema, crear herramientas puede ser tan importante como escribir tests, y a veces más
- Depurar se parece mucho a poner en práctica el método científico
- Se reúnen datos
- Se formulan predicciones
- Se validan con experimentos
- Se actualizan las predicciones con nuevos datos
- Después de esto, el proyecto pasa a problemas de un nivel de abstracción más alto
- Malentendidos de RFC
- Escritura de código con multitarea
- Y nuevos bugs en el camino
1 comentarios
Opiniones de Hacker News
La capacidad de crear pequeñas herramientas por cuenta propia es un superpoder y suele estar en el núcleo de lo que se llama un programador 10x.
Lamentablemente, este tipo de habilidad normalmente se ejerce en silencio, en lugares donde no llama la atención.
Pero para alguien con curiosidad y que piensa en efectos de segundo orden o en funciones que pronto harán falta, trabajar con desarrolladores que no hacen eso puede ser bastante doloroso.
Hace poco trabajé con alguien que no era mala persona, pero ahora estoy limpiando un proyecto que implementó al 100% solo lo que pedía el ticket. El nuevo proyecto, que pretendía reemplazar los problemas del software existente, conservaba exactamente todos los problemas anteriores por las mismas razones.
Dicho eso, la expresión desarrollador 10x suena un poco a palabra de moda. Me hace pensar en desarrolladores imprudentes que, sin supervisión, “terminan el trabajo” dejando grandes costos, con todo aislado en silos y que luego, cuando alguien más lo toca o esa persona se va, se derrumba como un castillo de naipes.
Idealmente, todos deberían tener tiempo de exploración, pero también hace falta un gerente que lo proteja estrictamente incluso cuando la alta dirección dice “hay que terminar más rápido”. Se vuelve más difícil si el gerente de otro equipo pregunta: “¿A ese equipo le van a permitir contratar más gente para compensar el tiempo perdido? Nosotros también necesitamos ese personal”.
Al final se necesita un mecanismo a nivel organizacional, e incluso Google abandonó el 20% del tiempo.
La solución “poco ética” es incluir un poco de ese tiempo dentro de las estimaciones de desarrollo.
Crear tus propias herramientas es útil cuando aplica, pero también creo que un desarrollador productivo escribe la menor cantidad de código posible y aprovecha lo que ya existe. En el ejemplo del artículo, nadie necesita crear desde cero una pila TCP propia; ya existen implementaciones excelentes.
Aun así, hacerla uno mismo puede ser la mejor forma de lograr una comprensión profunda, y esa comprensión profunda es uno de los componentes del mítico desarrollador 10x. Solo que no esperaría que el empleador pague por ese proceso.
Normalmente, si es algo que se termina en menos de un día, simplemente lo hago. Nunca me salió mal ni fue una pérdida de tiempo. En el peor de los casos, después termino copiando y pegando ese código en otra parte.
Luego empezó a pedirme que las hiciera usables por personas que no programan, y de repente esas herramientas se convirtieron en algo mucho más grande de lo previsto.
El título es bastante ambiguo, pero este artículo es el inicio de una serie sobre crear desde cero una pila de TCP/IP y framing Ethernet para microcontroladores.
El autor usa un chip W5100 que puede encargarse de TCP/IP por sí mismo, pero también soporta el modo de pasarle frames Ethernet ya construidos. Eso sí, el chip se encarga del preámbulo y del cálculo del CRC.
La mayor parte del artículo trata sobre comunicarse con el propio chip y enviar un paquete de prueba. Parece un paquete hardcodeado, aunque el artículo no lo dice explícitamente.
Personalmente esperaba que fuera sobre hacer bit banging de Ethernet en un hardware absurdo.
Uno de los trucos es modificar una placa común con PHY + MagJack para que el RP2040 genere la señal de reloj. Así la señal queda sincronizada y no hace falta sobremuestrear RMII.
Si solo necesitas 10 Mb/s, también se puede hacer de formas más sucias. Todavía estoy esperando una terminal de vidrio “moderna” que combine salida de video DVI/HDMI y Ethernet en un RP2040. Con telnet bastaría; SSH probablemente sea demasiado.
Hace poco hice un cambio de carrera algo inusual hacia la ingeniería FPGA centrada en Ethernet.
Fue un viaje interesante y al final llegué a diseñar mi propio IP de Hard MAC y enviar paquetes a través de un IP de PHY personalizado.
Se lo recomiendo mucho a quienes quieran intentar este desafío en “modo difícil”. Las redes están demasiado abstraídas para el usuario, así que fue realmente valioso entender cómo las tarjetas Ethernet, los módems y los switches ensamblan y desarman cada parte de un paquete, y cómo PHY/PCS recupera la señal en el enlace.
Para depuración y verificación, permitimos conectar una versión de simulación hecha con Verilator a un dispositivo TUN/TAP de Linux, de modo que era posible conectarse directamente desde la máquina del desarrollador sin ocupar el hardware físico. Fue especialmente útil porque solo había una unidad de hardware.
Mi conocimiento de redes se queda en una comprensión muy básica del modelo OSI, pero quiero profundizar en el mundo de las redes.
Es la primera vez que veo una reinterpretación de MOSI/MISO: en vez de master out/slave in, lo llaman main out/subordinate in.
Así se pueden seguir llamando MOSI/MISO a los pines, así que creo que yo también lo usaría. La alternativa COPI/CIPO (controller out/peripheral in) nunca se me quedó en la cabeza.
Me resulta curioso que el autor use un shield Ethernet W5100 y un STM32F401. Con una facilidad similar podría usar una placa STM32F407, que trae un MAC Ethernet integrado, y desarrollarla junto con una placa PHY Ethernet barata.
También hay muchos proyectos de ejemplo de Ethernet, y es tan fácil de desarrollar como con el STM32F401.
Además, creo que la explicación de que “por la complejidad del procesamiento de señales Ethernet normalmente se usa un ASIC dedicado” en general no aplica en el contexto de microcontroladores. En muchos casos, la funcionalidad Ethernet viene como periférico integrado dentro del microcontrolador, como en el STM32F407 o el ESP32.
Definitivamente tiene más sentido usar un chip con periférico Ethernet integrado. Aunque también implica cambiar la complejidad de configurar el W5100 por la complejidad de configurar los periféricos de ST.
El código de red ya abstrae el chip real detrás de una interfaz de driver (tipo read/write/ioctl), así que el port debería ser bastante sencillo.
En esta serie voy a evaluar el STM32F407.
Ethernet no maneja paquetes, sino tramas.
El paquete es un concepto de IP.
Sin embargo, el RFC 791 es un documento de una época en la que la red L2 subyacente probablemente era ARPAnet, que usaba paquetes de 128 bytes.
Hoy esa distinción en la práctica debería no importar. Si estás dependiendo de la fragmentación IP para enviar un datagrama IP grande como varios paquetes L2, algo está mal. IPv6 ni siquiera soporta fragmentación dentro de la red.
En la práctica, con el tiempo la distinción casi desapareció. Normalmente una trama Ethernet contiene un datagrama IP, y todo el mundo simplemente lo llama paquete.
https://en.wikipedia.org/wiki/Ethernet_frame
Si quieres probar Ethernet cableado en un microcontrolador, algunas de las placas STM32 Nucleo más grandes traen Ethernet de 100 Mbps integrado y son bastante baratas, alrededor de 25 dólares.
Las opiniones sobre el software STM32Cube son mixtas, pero te genera un ejemplo funcional de comunicación Ethernet.
https://www.st.com/en/evaluation-tools/nucleo-f439zi.html
Viene con MQTT, cliente y servidor HTTP, etc.
La última vez que usé Cube-MX, la experiencia en general fue muy desagradable. Si volviera a usar STM32, creo que preferiría stm32-hal o libopencm3. La herramienta en sí y el código generado tenían todo tipo de bugs malos y casos límite, y pasé días depurando. Quizá ahora haya mejorado.
https://github.com/egnor/wt32-eth01
https://liliputing.com/waveshare-esp32-p4-nano-is-a-tiny-ris...
Acabo de ver que también hay módulos más baratos. Cosas como el WT32-ETH01 quizá tengan menos rendimiento.
Si quieres escribir tu propio stack de red en Linux, puedes usar
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))y trabajar a un nivel de abstracción no muy distinto al de este artículo.Los paquetes
SOCK_RAWse envían y reciben hacia/desde el driver del dispositivo sin modificar los datos del paquete. Al recibir, la dirección se parsea y se entrega en una estructura de dirección estándarsockaddr_ll.Al enviar, el buffer provisto por el usuario debe contener el encabezado de la capa física, y ese paquete entra sin cambios en la cola del driver de red de la interfaz especificada por la dirección de destino.
https://man7.org/linux/man-pages/man7/packet.7.html
Esto agrega una interfaz virtual al stack de red, como una interfaz VPN. En cambio, los sockets de paquetes te permiten comunicarte directamente con una interfaz real.
Más precisamente, creo que habría que decir tramas Ethernet.
Muy bien hecho. Yo acabo de pasar las últimas 16 horas de trabajo haciendo lo contrario: una implementación que parsea Ethernet II (incluyendo VLAN), IPv4+6 y UDP hasta llegar a un protocolo IP automotriz.
El objetivo es entender un stream de captura de bus propietario, que transporta tramas Ethernet anidadas dentro de tramas Ethernet, etc., y yo lo recibo mediante sockets raw.
Para este tipo de trabajo, Wireshark y ChatGPT son realmente invaluables.