2 pontos por GN⁺ 2024-11-12 | 1 comentários | Compartilhar no WhatsApp
  • Como primeiro passo para construir uma pilha TCP/IP diretamente em um microcontrolador, foi feita uma tentativa de transmitir um quadro Ethernet conectando um STM32F401 Nucleo a um shield W5100 da Wiznet
  • Sem usar os recursos de TCP/IP em hardware do W5100, foi usado apenas o modo MAC Raw, deixando para o chip somente o processamento de baixo nível necessário para a transmissão de quadros
  • O primeiro obstáculo começou porque a fiação SPI do Arduino Ethernet shield não era compatível com o Nucleo, e isso foi resolvido após verificar o roteamento do conector ICSP e fazer uma modificação na fiação da placa
  • Depois, passando por respostas anômalas no MISO que pareciam um problema de temporização do chip select e por pacotes lixo no Wireshark, um analisador lógico e a comparação com uma implementação de referência se tornaram as principais ferramentas de depuração
  • A causa final foi um bug em w5100_write16() que reescrevia o segundo byte no mesmo endereço, e graças à criação de uma ferramenta de análise de CSV de captura SPI foi possível chegar até a transmissão correta do pacote

Ponto de partida para criar uma pilha TCP/IP do zero

  • O objetivo é iniciar a série “Networking from scratch”, implementando uma pilha TCP/IP desde a base em cima de um microcontrolador
  • O resultado aparente desta etapa é o primeiro envio de um pacote Ethernet, mas o foco real está no processo de rastrear bugs surgidos entre o hardware e o driver
  • A placa usada foi uma placa de desenvolvimento Nucleo baseada em STM32F401
    • ARM Cortex-M4
    • operação de até 84MHz
    • 96KiB de RAM
    • memória considerada suficiente para armazenar vários pacotes

O papel do Ethernet e do W5100

  • Ethernet não é apenas uma porta ou um formato de quadro, mas uma família de tecnologias e padrões que inclui hardware da camada física, formas de sinalização, tratamento de colisões no barramento e organização dos quadros
  • Como o processamento de sinais Ethernet é complexo, normalmente um ASIC dedicado recebe dados em nível de quadro e cuida do tratamento dos sinais elétricos no cabo
  • No projeto, foi usado um Arduino Ethernet shield com o chip W5100 da Wiznet
    • A placa usada era uma cópia barata, então foi preciso fazer modificações para funcionar corretamente
  • O W5100 é um chip que incorpora uma pilha TCP/IP em hardware a um ASIC Ethernet
    • oferece 4 “sockets”
    • pode ser configurado nos níveis TCP, UDP, IP e “MAC Raw”
  • Como o objetivo era implementar a pilha TCP/IP manualmente, os recursos de TCP/IP do W5100 não foram usados, e apenas um socket foi usado em modo MAC Raw
    • o usuário entrega um quadro Ethernet
    • o W5100 executa a transmissão real
    • o preâmbulo e o marcador de início de quadro são elementos de nível elétrico, então ficam a cargo do chip
    • o CRC de 32 bits também é calculado pelo W5100

Problema 1: o sinal SPI não chegava ao W5100

  • A troca de dados com o W5100 é feita por SPI
    • MOSI: saída do chip principal, o microcontrolador
    • MISO: saída do W5100
    • clock: relógio de referência dos dados
    • chip select: sinal que informa que a comunicação está acontecendo com o chip secundário
  • O datasheet do W5100 define, sobre o SPI, um protocolo de comando de 4 bytes
    • 1 byte de operação
    • 2 bytes de endereço 16-bit big-endian
    • 1 byte de valor
  • A operação pode ser escrita 0xf0 ou leitura 0x0f
    • outros valores não são válidos e devem ser ignorados
  • O W5100 devolve valores conhecidos no MISO a cada byte deslocado pelo clock, o que facilita identificar problemas de comunicação
    • em um comando de leitura, o 4º byte passa a ser o valor lido do endereço especificado
  • No início, mesmo enviando comandos pelo MOSI, apareciam valores lixo no MISO
  • A causa era o fato de o Arduino Ethernet shield encaminhar os sinais SPI não pelo cabeçalho padrão do Arduino, mas pelo conector ICSP de 6 pinos
    • em placas Arduino oficiais, os sinais SPI do ICSP e do cabeçalho padrão são conectados internamente, então não há problema
    • como a placa Nucleo não tinha conector ICSP, os sinais SPI enviados não chegavam ao W5100
  • O problema de conexão foi confirmado medindo com um multímetro a resistência entre as linhas de alimentação e os sinais SPI
    • resistência infinita indica que a fiação está interrompida
  • Os sinais entre o Nucleo e o W5100 foram ligados diretamente com solda e fio de cobre esmaltado

Problema 2: temporização do chip select e respostas anômalas no MISO

  • Depois que os sinais SPI foram realmente conectados, a comunicação com o W5100 passou a funcionar e foi possível implementar até a etapa seguinte
    • configuração da transmissão raw Ethernet no W5100
    • configuração do endereço MAC
    • configuração dos segmentos de memória TX/RX
    • escrita de um quadro Ethernet de teste na memória TX e acionamento da transmissão
  • O Nucleo e o shield foram conectados a um notebook com um cabo CAT5 e o Wireshark foi executado, mas nenhum pacote apareceu
  • Problemas de baixo nível são difíceis de rastrear porque não há mensagem de erro nem stack trace; é mais uma situação de “mexi nos elétrons, mas nada do que eu esperava aconteceu”
  • Para a depuração, foi usado um analisador lógico
    • ele amostra as transições high/low de sinais digitais em vários canais
    • o software interpreta grupos de sinais como um barramento SPI e mostra os bytes da transação
    • também pode exportar em formatos estruturados, como CSV
  • O equipamento usado foi um Saleae Logic 8
    • analisadores baratos na faixa de €10 também podem funcionar com o Saleae Logic2, mas há trade-offs para obter um funcionamento correto e consistente
  • O primeiro comando SPI parecia normal
    • 0xf0 0x00 0x00 0x80
    • define o bit mais alto do Mode Register no endereço 0x0000 para disparar um reset por software
    • o MISO respondia corretamente de 0x00 a 0x03
  • Já no comando de leitura seguinte apareceu uma resposta estranha
    • MOSI: 0x0f 0x00 0x00 0x00
    • MISO: 0x03 0xff 0xff 0xff
  • Como 0x03 era o último valor da transação anterior, surgiu a suspeita de um problema de estado interno ou de temporização no W5100
  • O clock SPI estava sendo usado muito abaixo do máximo de cerca de 14MHz indicado no datasheet, então isso não foi considerado a causa
  • Em vez disso, considerou-se que o chip select estava voltando para high rápido demais e deixando o chip em um estado ruim, então foi inserido um atraso de alguns microssegundos antes da mudança de estado
    • depois disso, a resposta no MISO voltou ao normal
    • os valores configurados também passaram a ser lidos novamente com resultados razoáveis
  • A causa exata desse problema ainda não foi totalmente esclarecida
    • as restrições de chip select mostradas no diagrama de temporização SPI da página 66 do datasheet estavam sendo respeitadas
    • as condições exatas de contorno podem ser investigadas depois, mas por enquanto a prioridade foi seguir com o projeto

Problema 3: pacotes lixo aparecendo no Wireshark

  • Ao executar o Wireshark novamente, os pacotes apareceram, mas não eram os pretendidos
  • No momento em que o microcontrolador emitia o comando de transmissão, surgia um pacote Ethernet raw muito maior que o original e preenchido com dados lixo
  • O plano de implementar tudo apenas com base no datasheet e na especificação foi abandonado, e nesta etapa a decisão foi comparar com uma implementação comprovadamente funcional
  • O Arduino é útil para criar rapidamente uma implementação de referência
    • com Arduino, bibliotecas e cerca de 5 linhas de código, dá para validar comportamentos complexos
  • Encontrar uma biblioteca para envio de pacotes raw Ethernet levou mais tempo
    • a maioria dos usuários de Arduino não tenta implementar as funções de rede do zero
    • a biblioteca oficial removeu do API público o suporte para envio de pacotes raw
  • Foi encontrado o projeto GitHub W5100MacRaw, que faz envio e recepção de pacotes com o mínimo de etapas
  • O código-fonte foi comparado com a implementação própria, mas as diferenças visíveis de imediato não pareciam decisivas
    • a ordem de leitura/escrita de registradores era diferente
    • havia leituras/escritas presentes só em um dos lados
    • normalmente, se a ordem fosse dependente, isso estaria explícito no datasheet
  • Mesmo ajustando a ordem de leitura/escrita para ficar igual à implementação de referência, o Wireshark continuava mostrando pacotes lixo

O bug real revelado por uma pequena ferramenta

  • A estratégia seguinte foi escrever uma ferramenta para analisar o CSV de captura SPI do Saleae Logic2 e reapresentá-lo como uma lista de leituras/escritas de registradores
  • A ferramenta foi feita em Python e tinha pouco mais de 200 linhas no total
    • a maior parte era composta por nomes e endereços de registradores copiados do datasheet
    • levou cerca de 1 hora para ser escrita
    • também foi incluído parsing de argumentos para deixar entrada e saída mais claras
  • Foram feitas capturas SPI tanto da implementação de referência em Arduino quanto da implementação própria, exportadas em CSV, processadas com a ferramenta e comparadas com diff
  • O problema estava na função utilitária w5100_write16(u16 address, u16 value)
    • muitos registradores do W5100 usam valores de 16 bits
    • como o formato de comando só permite escrever 8 bits por vez, é preciso dividir em byte alto e byte baixo
    • essa função não escrevia o segundo byte em address + 1, mas o reescrevia no mesmo endereço
  • O log da implementação de referência em Arduino escrevia separadamente em S0_TX_WR0 e S0_TX_WR1
    • S0_TX_WR0 [0x0424] 0x00
    • S0_TX_WR1 [0x0425] 0x3c
  • No log da implementação própria, ambos eram escritos em S0_TX_WR0 [0x0424]
    • S0_TX_WR0 [0x0424] 0x00
    • S0_TX_WR0 [0x0424] 0x3c
  • Esse registrador era o ponteiro de escrita de transmissão do Socket 0
    • o W5100 espera ler esse registrador de 16 bits em sequência
    • escrever os bytes na memória TX
    • regravar o novo ponteiro de escrita
    • e então receber o comando de envio do socket
  • Ao executar o envio sem gravar o segundo byte, o chip entrava em um estado anômalo indefinido, e no Wireshark o resultado aparecia como pacotes aparentemente aleatórios
  • Depois de corrigir a função, o pacote de teste passou a aparecer normalmente no Wireshark

O valor do tempo gasto criando ferramentas de depuração

  • O envio do primeiro pacote Ethernet em si não é uma conquista gigantesca, mas dentro do projeto foi um momento que pôde ser claramente tratado como um sucesso
  • Revisitar bugs é divertido em projetos pessoais, e gastar tempo com criação de ferramentas e exploração do espaço de depuração quase sempre vale a pena
  • Em alguns ambientes profissionais, existe a tendência de tratar como desperdício o trabalho que não produz diretamente um deliverable
    • quando o processo de desenvolvimento é fragmentado no estilo JIRA, tarefas que não preenchem checkboxes diretamente tendem a ser menos valorizadas
    • quando o sistema ainda não foi compreendido o suficiente, criar ferramentas pode ser tão importante quanto escrever testes, e às vezes até mais
  • Depuração se parece muito com a aplicação do método científico
    • coletar dados
    • formular previsões
    • validar previsões com experimentos
    • atualizar previsões com novos dados
  • Depois disso, o projeto avança para problemas em níveis mais altos de abstração
    • interpretações erradas de RFCs
    • escrita de código multitarefa
    • e novos bugs em andamento

1 comentários

 
GN⁺ 2024-11-12
Opiniões no Hacker News
  • A capacidade de criar pequenas ferramentas por conta própria é um superpoder e costuma estar no cerne do que se chama de programador 10x
    Infelizmente, esse tipo de habilidade normalmente aparece em silêncio, em lugares que não chamam atenção

    • Eu também já passei por esse tipo de situação. Organizações também precisam de desenvolvedores que funcionem como engrenagens, e em muitas organizações essa é, na prática, a única forma de as coisas funcionarem
      Mas, para alguém curioso e que pensa em efeitos de segunda ordem ou em funcionalidades que logo serão necessárias, trabalhar com desenvolvedores que não fazem isso pode ser bem doloroso
      Uma pessoa com quem trabalhei recentemente não era má pessoa, mas agora estou limpando um projeto em que ela implementou 100% apenas o que o ticket mandava. O novo projeto, que deveria substituir os problemas do software existente, acabou mantendo todos os problemas antigos exatamente pelos mesmos motivos
      Dito isso, a expressão desenvolvedor 10x tem um certo cheiro de buzzword. Ela me faz pensar em desenvolvedores imprudentes que “entregam o trabalho” sem supervisão, deixam grandes custos para trás, siloam tudo e, quando alguém precisa mexer depois ou quando a pessoa sai, tudo desaba como um castelo de cartas
    • Vendo pelo outro lado, enquanto Frank está enviando pacotes Ethernet, outras pessoas precisam assumir o trabalho original necessário para a organização como um todo. Como o backlog de problemas reais a resolver é praticamente infinito, também dá para argumentar: por que não inovar ali?
      Idealmente, todos deveriam ter tempo de exploração, mas é preciso um gestor que proteja isso com rigor mesmo quando a alta administração diz que “precisa terminar mais rápido”. Fica ainda mais difícil se o gerente de outro time disser: “esse time vai poder contratar mais gente para compensar o tempo perdido? Nós também precisamos dessas pessoas”
      No fim, é necessário um mecanismo em nível organizacional, e até o Google abandonou o 20% time
      A solução “antiética” é embutir um pouco desse tempo nas estimativas de desenvolvimento
    • Desenvolvedores 10x geralmente não são pessoas presas à prioridade máxima definida por um gerente ou responsável de produto. Não sei se é porque se recusam a receber ordens, porque elas mesmas são gerentes, ou porque nem havia gerente em primeiro lugar
      Criar suas próprias ferramentas é útil quando a situação permite, mas também acho que um desenvolvedor produtivo escreve o mínimo de código possível e aproveita o que já existe. No exemplo do artigo, ninguém precisa recriar uma pilha TCP do zero; já existem ótimas implementações
      Ainda assim, fazer por conta própria pode ser a melhor forma de obter um entendimento profundo, e esse entendimento profundo é parte do mítico desenvolvedor 10x. Só não espere que o empregador pague por esse processo
    • Eu já vi reações nos dois extremos. Houve respostas como “uau, isso é muito útil; essa ferramenta validou nosso modelo”, e também “você perguntou ao PM antes? Cuidado para não cair numa toca de coelho”
      Em geral, se é algo que termina em menos de um dia, eu simplesmente faço. Nunca deu errado e nunca foi desperdício. No pior caso, acabo copiando e colando aquele código em outro lugar mais tarde
    • Recentemente, no trabalho, criei um conjunto de pequenas ferramentas que mistura Python e shell e roda no WSL, e meu chefe ficou bem impressionado ao me ver depurando o sistema IoT de um cliente
      Aí ele começou a pedir que eu as tornasse utilizáveis por não programadores, e de repente essas ferramentas viraram um trabalho muito maior do que o esperado
  • O título é bem ambíguo, mas este artigo é o início de uma série sobre criar do zero uma pilha de TCP/IP e framing Ethernet para microcontroladores
    O autor usa um chip W5100, que consegue lidar com TCP/IP por conta própria, mas também suporta receber quadros Ethernet já montados. Ainda assim, o preâmbulo e o cálculo do CRC ficam por conta do chip
    A maior parte do texto trata da comunicação com o próprio chip e do envio de um pacote de teste. Parece ser um pacote hardcoded, mas o texto não afirma isso explicitamente
    Pessoalmente, eu esperava que fosse sobre fazer bit banging de Ethernet em um hardware absurdo

    • O RP2040 consegue fazer bit banging de Ethernet se houver apenas um transceiver, inclusive a 100 Mb/s
      Um dos truques é modificar uma placa comum com PHY + MagJack para que o RP2040 gere o sinal de clock. Assim o sinal fica sincronizado e não é preciso fazer oversampling de RMII
      Se você só precisa de 10 Mb/s, dá para fazer de formas ainda mais sujas. Ainda estou esperando por um terminal de vidro “moderno” que combine saída de vídeo DVI/HDMI e Ethernet no RP2040. Só telnet já seria suficiente; SSH provavelmente seria demais
    • Anos atrás, alguém fez isso com um ATTiny85: https://hackaday.com/2014/08/29/bit-banging-ethernet-on-an-a...
  • Recentemente fiz uma mudança de carreira um tanto incomum para engenharia de FPGA centrada em Ethernet
    Foi uma jornada divertida e, no fim, cheguei a projetar meu próprio Hard MAC IP e enviar pacotes por meio de um PHY IP customizado
    Recomendo muito para quem quiser encarar esse desafio no “modo difícil”. Redes são abstratas demais para o usuário, então foi realmente valioso entender como placas Ethernet, modems e switches montam e desmontam cada parte dos pacotes, e como PHY/PCS recuperam o sinal no link

    • Cerca de 10 anos atrás, participei de um projeto que processava TCP diretamente em um FPGA
      Para depuração e validação, permitimos conectar a versão simulada feita com Verilator a um dispositivo TUN/TAP do Linux, o que possibilitava acessar diretamente a partir da máquina do desenvolvedor sem ocupar o hardware físico. Isso foi especialmente útil porque só havia uma unidade de hardware
    • Fico curioso se você já era familiarizado com redes ou Ethernet quando começou. Se não, também gostaria de saber que materiais usou para entender o panorama geral
      Meu conhecimento de redes termina em uma compreensão muito básica do modelo OSI, mas quero me aprofundar no mundo das redes
  • Foi a primeira vez que vi uma reinterpretação de MOSI/MISO: em vez de master out/slave in, chamar de main out/subordinate in
    Assim os nomes dos pinos podem continuar sendo MOSI/MISO, então acho que eu também usaria. A alternativa COPI/CIPO (controller out/peripheral in) nunca ficou muito bem na minha cabeça

  • Acho estranho o motivo de o autor usar um shield Ethernet W5100 e um STM32F401. De forma igualmente simples, daria para usar uma placa STM32F407, que tem MAC Ethernet integrado, e desenvolvê-la junto com uma placa Ethernet PHY barata
    Há muitos projetos de exemplo de Ethernet, e é tão fácil de desenvolver quanto o STM32F401
    Além disso, acho que a explicação de que “por causa da complexidade do processamento de sinais Ethernet, normalmente se usa um ASIC dedicado” em geral não se aplica muito bem no contexto de microcontroladores. Em muitos casos, a funcionalidade Ethernet vem como periférico integrado dentro do microcontrolador, como no STM32F407 ou no ESP32

    • Sou o autor. O motivo é bem sem graça: quando decidi começar o projeto, eram essas as peças que eu tinha à mão
      Usar um chip com periférico Ethernet integrado certamente faz mais sentido. Mas também significa trocar a complexidade de configurar o W5100 pela complexidade de configurar os periféricos da ST
      Como o código de rede já abstrai o chip real por trás de uma interface de driver (algo como read/write/ioctl), o porte deve ser bem simples
      Nesta série, vou avaliar o STM32F407
    • Para algumas pessoas, o divertido é o próprio processo de aprender, não necessariamente fazer do jeito mais moderno
    • Hoje dei uma olhada nas placas ESP32 com Ethernet e pareceu que a maioria, ou uma parcela considerável delas, usa Ethernet como um dispositivo serial
  • Ethernet lida com frames, não pacotes
    Pacote é um conceito de IP

    • A RFC 791, que definiu o IP, fala tanto em pacotes quanto em datagramas. O IP envia datagramas, e cada datagrama IP é fragmentado em um ou mais pacotes dependendo da rede L2 subjacente
      Mas a RFC 791 é um documento de uma época em que era muito provável que a rede L2 subjacente fosse a ARPAnet, que usava pacotes de 128 bytes
      Hoje essa distinção deveria ser, na prática, irrelevante. Se você está contando com fragmentação IP para enviar um datagrama IP grande em vários pacotes L2, há algo errado. IPv6 nem sequer dá suporte à fragmentação dentro da rede
      Na prática, com o tempo, a distinção quase desapareceu. Normalmente um frame Ethernet contém um datagrama IP, e todo mundo simplesmente chama de pacote
    • Coisa que só acontece no HN. Alguém implementou networking do zero, e nos comentários rebaixam a pessoa como se ela não soubesse o que está fazendo por causa de um termo
    • Em Ethernet, pacote é um conceito da camada física e encapsula o frame, que é um conceito da camada de enlace de dados
      https://en.wikipedia.org/wiki/Ethernet_frame
    • Pacote é um conceito de TCP, e IP envia datagramas
  • Se você quiser experimentar Ethernet cabeada em um microcontrolador, algumas das placas STM32 Nucleo maiores têm Ethernet de 100 Mbps integrada e, por cerca de 25 dólares, são bem baratas
    As avaliações do software STM32Cube são mistas, mas ele gera um exemplo funcional de comunicação Ethernet
    https://www.st.com/en/evaluation-tools/nucleo-f439zi.html

    • Se você preferir algo baseado em ESP32, a WT32-ETH01 custa cerca de 7 dólares no Aliexpress. Pessoalmente, achei tão fácil, ou até mais fácil, começar com ela do que com as peças da STM
      Ela vem com MQTT, cliente e servidor HTTP etc.
      Da última vez que usei o Cube-MX, a experiência geral foi muito desagradável. Se eu voltasse a usar STM32, acho que preferiria stm32-hal ou libopencm3. Passei dias depurando todo tipo de bug ruim e casos de borda tanto na própria ferramenta quanto no código gerado. Talvez esteja melhor agora
      https://github.com/egnor/wt32-eth01
    • Uma placa RISC-V ESP32-P4 com Ethernet por cerca de 20 dólares é interessante
      https://liliputing.com/waveshare-esp32-p4-nano-is-a-tiny-ris...
      Acabei de ver que também existem módulos mais baratos. Coisas como a WT32-ETH01 podem ter menos desempenho
    • O STM32Cube até gera exemplos funcionais de comunicação Ethernet, mas em alguns hardwares eles estão quebrados
    • Felizmente, há vários projetos CMake alternativos no GitHub
    • O STM32Cube tem uma vibe bem clara de “anos 90”
  • Se você quiser escrever sua própria pilha de rede no Linux, pode usar socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) para trabalhar em um nível de abstração não muito diferente do deste texto
    Pacotes SOCK_RAW são trocados com o driver do dispositivo sem modificar os dados do pacote. Na recepção, o endereço é analisado e entregue em uma estrutura de endereço padrão sockaddr_ll
    No envio, o buffer fornecido pelo usuário precisa conter o cabeçalho da camada física, e esse pacote entra sem alterações na fila do driver de rede da interface especificada pelo endereço de destino
    https://man7.org/linux/man-pages/man7/packet.7.html

    • Para fazer no sentido oposto, é fácil abrir uma interface tun (IP virtual) ou tap (Ethernet virtual)
      Isso adiciona uma interface virtual à pilha de rede, como uma interface de VPN. Já os sockets de pacote, ao contrário, permitem se comunicar diretamente com uma interface real
  • Tecnicamente, acho que deveria ser frame Ethernet

  • Ficou bem feito. Eu acabei de passar as últimas 16 horas de trabalho fazendo a operação inversa: uma implementação que faz parsing de Ethernet II (incluindo VLAN), IPv4+6 e UDP até um protocolo IP automotivo
    O objetivo é entender um stream de captura de barramento proprietário; esse stream carrega frames Ethernet aninhados dentro de frames Ethernet, e eu os recebo por socket bruto
    Para esse tipo de trabalho, Wireshark e ChatGPT são realmente inestimáveis