2 puntos por GN⁺ 2024-11-12 | 1 comentarios | Compartir por WhatsApp
  • 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 0xf0 o lectura 0x0f
    • 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 0x0000 para disparar un software reset
    • MISO respondió correctamente de 0x00 a 0x03
  • En el siguiente comando de lectura apareció una respuesta anómala
    • MOSI: 0x0f 0x00 0x00 0x00
    • MISO: 0x03 0xff 0xff 0xff
  • Como 0x03 era 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_WR0 y S0_TX_WR1
    • S0_TX_WR0 [0x0424] 0x00
    • S0_TX_WR1 [0x0425] 0x3c
  • En el log propio, ambos se escribían en S0_TX_WR0 [0x0424]
    • S0_TX_WR0 [0x0424] 0x00
    • S0_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

 
GN⁺ 2024-11-12
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.

    • Yo también he pasado por situaciones así. Las organizaciones también necesitan desarrolladores tipo engranaje, y en muchas organizaciones esa es la única forma en que las cosas realmente funcionan.
      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.
    • Visto al revés, mientras Frank está enviando paquetes Ethernet, otras personas tienen que hacer en su lugar el trabajo real que necesita toda la organización. Como el backlog de problemas reales por resolver es casi infinito, también se puede argumentar: ¿por qué no innovar ahí?
      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.
    • Muchas veces, los desarrolladores 10x no son personas atadas a la máxima prioridad definida por un gerente o responsable de producto. No sé si es porque se niegan a recibir órdenes, porque ellos mismos son gerentes o porque, para empezar, no tienen gerente.
      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.
    • Yo también he recibido reacciones de ambos extremos. Algunas fueron: “Guau, esto es realmente útil. Esta herramienta validó nuestro modelo”; otras: “¿Ya le preguntaste al PM? Hay que tener cuidado de no meterse en una madriguera”.
      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.
    • Hace poco, en el trabajo, armé un conjunto de pequeñas herramientas mezclando Python y shell que corre en WSL, y mi jefe quedó bastante impresionado al verme depurar el sistema IoT de un cliente.
      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.

    • El RP2040 puede hacer bit banging de Ethernet con solo tener un transceptor, e incluso a 100 Mb/s.
      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 tiempo alguien hizo algo así con un ATTiny85: https://hackaday.com/2014/08/29/bit-banging-ethernet-on-an-a...
  • 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.

    • Hace unos 10 años participé en un proyecto que manejaba TCP directamente en un FPGA.
      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.
    • Me da curiosidad saber si al empezar ya estabas familiarizado con redes o Ethernet. Si no, también me gustaría saber qué recursos usaste para captar el panorama completo.
      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.

    • Soy el autor. La razón es bastante anticlimática: cuando decidí empezar el proyecto, esas eran las piezas que tenía a mano.
      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.
    • Para algunas personas, lo divertido es simplemente el proceso de aprendizaje, más que hacerlo de la forma más moderna.
    • Hoy estuve viendo placas ESP32 con Ethernet, y me pareció que la mayoría, o una buena parte, usan Ethernet como si fuera un dispositivo serial.
  • Ethernet no maneja paquetes, sino tramas.
    El paquete es un concepto de IP.

    • El RFC 791, que definió IP, habla tanto de paquetes como de datagramas. IP envía datagramas, y cada datagrama IP se fragmenta en uno o más paquetes según la red L2 subyacente.
      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.
    • Esto solo pasa en HN. Alguien implementa networking desde cero, y en los comentarios lo menosprecian por un término, como si no supiera lo que está haciendo.
    • En Ethernet, un paquete es un concepto de la capa física, y encapsula una trama, que es un concepto de la capa de enlace de datos.
      https://en.wikipedia.org/wiki/Ethernet_frame
    • Paquete es un concepto de TCP; IP envía datagramas.
  • 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

    • Si quieres algo basado en ESP32, el WT32-ETH01 cuesta unos 7 dólares en Aliexpress. Personalmente, me resultó igual o más fácil de empezar a usar que las piezas de STM.
      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
    • Me parece interesante que haya una placa RISC-V ESP32-P4 con Ethernet por unos 20 dólares.
      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.
    • STM32Cube a veces genera un ejemplo funcional de comunicación Ethernet, pero en cierto hardware está roto.
    • Por suerte, en GitHub hay varios proyectos CMake alternativos.
    • STM32Cube tiene una sensación muy marcada de “años 90”.
  • 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_RAW se 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ándar sockaddr_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

    • Para hacerlo en la dirección opuesta, puedes abrir fácilmente una interfaz tun (IP virtual) o tap (Ethernet virtual).
      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.