3 pontos por GN⁺ 2024-04-10 | 1 comentários | Compartilhar no WhatsApp
  • Um projeto de hardware que adiciona transmissão e recepção Ethernet 10BASE-T a um computador de lógica discreta feito sem CPU comercial nem chip de rede dedicado
  • Sobre um adaptador de camada física 10BASE-T↔SPI criado anteriormente, foi adicionado um módulo de camada MAC para conectá-lo ao homebrew computer, com transmissor e receptor em uma arquitetura full-duplex independente
  • O receptor converte dados SPI em bytes e os armazena em uma SRAM de 2 kB, verificando em hardware os 6 primeiros bytes para aceitar apenas FE:FA:F6:F2:EE:EA ou MAC de broadcast
  • Para simplificar o circuito, o transmissor deixa a geração do FCS e a preparação do preâmbulo para o software e suporta apenas quadros de tamanho fixo de 1024 bytes
  • Foi criado até um compilador C capaz de compilar o uIP 1.0 para executar apps de rede; o resultado foi ping médio de 85 ms e download HTTP de arquivos estáticos em torno de 2.6 kB/s

Adicionando Ethernet a um computador de lógica discreta

  • Como continuação do trabalho de construir um sistema computacional completo com componentes de lógica discreta, foi implementado um adaptador Ethernet capaz de executar aplicações de rede
  • Antes, foi criado um adaptador de camada física que converte sinais 10BASE-T Ethernet para SPI e de volta, e na época um microcontrolador STM32 foi usado para testar o funcionamento
  • O ponto central deste trabalho é um módulo de camada MAC para conectar esse adaptador ao homebrew computer
  • O adaptador tem arquitetura full-duplex, e as partes de transmissão e recepção operam independentemente entre si

Receptor: armazenando dados SPI no buffer de quadros

  • O receptor converte dados seriais SPI em dados paralelos por byte e extrai o clock de byte
  • O endereço MAC de destino é verificado nos 6 primeiros bytes, e quadros que não atendem ao critério são rejeitados
  • Os bytes recebidos são gravados em um buffer SRAM 6116 de 2 kB
  • Quando o quadro termina, o receptor é desativado e não recebe quadros adicionais até ser reativado
  • O contador de bytes mantém o valor mesmo após parar, para que a CPU possa ler o tamanho recebido
  • O FCS não é verificado em hardware
  • Coleta de dados e acesso ao buffer

    • Os dados seriais SPI entram no registrador de deslocamento U32, enquanto U30 e U31 contam bits e bytes, respectivamente
    • O flip-flop D U29B gera o sinal de escrita na SRAM recv_buf_we, que vai brevemente para nível baixo a cada 8 bits de entrada
    • Os bytes recebidos são gravados na SRAM 6116 U20
    • U13, U16 e U18 formam um multiplexador de endereços que seleciona para a entrada de endereço da SRAM o contador de bytes ou o barramento de endereços do sistema
    • U21 atua como um buffer tri-state para enviar os bytes recebidos à RAM
    • A RAM e o contador de bytes são conectados ao barramento de dados do sistema para que a CPU possa acessar os dados recebidos e seu comprimento
    • U25 conecta a RAM de recepção ao barramento de dados do sistema
    • Após a conclusão do quadro, o valor do contador de bytes é mantido no barramento recv_byte_cnt
    • U26 e U27 enviam esse valor ao barramento de dados do sistema quando há uma solicitação de leitura de endereço específico
    • A metade restante de U27 forma um registrador de status somente leitura de 2 bits para consultar o estado do receptor e do transmissor

Filtragem de endereço MAC em hardware

  • Ao analisar o tráfego Ethernet, foi observado que os quadros normalmente chegavam em pequenos grupos de 3 a 4, separados por curtos atrasos, e mesmo dentro do mesmo grupo muitas vezes tinham endereços MAC de destino diferentes
  • Como o computador talvez não fosse rápido o bastante para fazer a filtragem MAC por software e reativar o receptor, foi necessária filtragem MAC em hardware
  • A ideia de armazenar um endereço MAC personalizado e compará-lo com os 6 primeiros bytes foi descartada por ser complexa demais
  • Um MAC com repetição de um único byte também era possível, mas no fim o endereço MAC foi construído como função do índice do byte
    • bit 0 é fixado em 0
    • bit 1 é fixado em 1
    • bits 2 a 4 são o valor invertido do índice do byte
    • bits 5 a 7 são fixados em 1
  • O endereço MAC gerado por essa regra é FE:FA:F6:F2:EE:EA
  • Para o funcionamento do ARP, também é aceito o MAC de broadcast FF:FF:FF:FF:FF:FF
  • U33 compara se o bit 0 e os bits 2 a 4 dos dados correspondem aos valores esperados, e a saída de U34A fica alta quando esses bits coincidem
  • U35A implementa a verificação do MAC de broadcast, e sua saída fica alta quando o bit 0 e os bits 2 a 4 são todos 1
  • Os dois sinais são combinados com um OR por diodos usando D7 e R6, e U35B verifica se todos os bits restantes também são 1
  • O resultado de validade de um único byte é acumulado em U10A
    • Quando nenhum quadro está sendo recebido, o sinal incoming SPI slave select ss fica baixo e U10A é definido como 1
    • Durante a recepção do quadro, o valor é atualizado a cada byte recebido
    • Se o endereço MAC de destino atender ao critério, o valor de U10A permanece alto
    • Quando o endereço de byte chega a 5, o valor final é armazenado em U36B, e a recepção do quadro é bloqueada se o endereço de destino não corresponder

Transmissor: simplificando o circuito com quadros de tamanho fixo

  • Assim como no receptor, o transmissor também não implementa em hardware a geração do FCS e deixa isso para o software
  • Para reduzir o circuito, o transmissor suporta apenas quadros de tamanho fixo
  • O comprimento do quadro foi definido como 1024 bytes, um valor próximo ao MTU comum de 1500 bytes
  • O preâmbulo exigido pelo 10BASE-T é composto por vários 0x55 e um 0xD5 no final, e o software deve carregá-lo junto dentro desses 1024 bytes
  • O comprimento fixo do quadro não afeta os protocolos de nível superior
    • Os protocolos superiores codificam o tamanho do pacote no cabeçalho
    • Eles não dependem do tamanho real do quadro Ethernet
  • Fluxo de dados de transmissão

    • Os dados de transmissão são armazenados em SRAM
    • Um clock de 20 MHz entra em um contador de 4 bits, e a saída de overflow é usada como clock de byte
    • Ao escrever qualquer valor em uma posição específica de memória write-only, o contador é ativado e a transmissão do quadro começa
    • Os dados paralelos em bytes são serializados por meio de um registrador de deslocamento
    • Como no receptor, U12 conta bits e U14 conta bytes
    • O clock de 20 MHz vem de um oscilador integrado e é usado apenas após pelo menos uma divisão por 2, sem uso direto
    • Isso evita que o duty cycle do oscilador afete o sinal de saída
  • RAM, registrador de deslocamento e temporização

    • Para selecionar a entrada de endereço da RAM U22, são usados três multiplexadores 74HC157, como no receptor
    • U23 é usado para carregar dados na RAM
    • U24 atua como um armazenamento intermediário para o byte atualmente em transmissão
    • O contador de bytes 74HC4040 é um ripple counter, então estabiliza lentamente
    • U24 fornece uma saída estável enquanto a saída da RAM ainda não é válida
    • Os dados entram no registrador de deslocamento U28 e são deslocados bit a bit
    • Houve um bug de hardware em que a ordem dos bits conectados da RAM para o registrador de deslocamento estava errada, então foi preciso contornar isso embaralhando os bits no software
    • MOSI e SCK precisam estar sincronizados com precisão para gerar um bom sinal 10BASE-T
    • U11A e U8B cuidam dessa sincronização
    • tx_cnt0 é o bit 0 do contador de bits e usa um sinal de clock dividido por 2 a partir de 20 MHz
    • U11A altera a saída de acordo com esse sinal
    • U8B atrasa o clock para acompanhar o atraso introduzido por U11A
    • Um latch D é mais complexo que uma simples porta AND e adiciona cerca de 5 ns de atraso, então foi usado um 74LV74A mais rápido
    • O 74LV74A é o único chip de família rápida nesta placa

Interface da CPU e mapeamento de memória

  • Do ponto de vista do programador, o adaptador Ethernet aparece como uma interface mapeada em memória
  • Os dois buffers de quadros são mapeados em 0xF000
  • Há dois registradores somente leitura
    • O registrador de status de 8 bits em 0xFB00 tem os flags RX_FULL e TX_BUSY
    • RX_FULL indica que a recepção de um quadro foi concluída
    • TX_BUSY indica que um quadro está sendo transmitido
    • O registrador de 16 bits em 0xFB02 contém o comprimento dos dados recebidos
  • As operações de escrita são usadas como comandos de controle
    • Escrever qualquer valor em 0xFB00 reativa o receptor
    • Escrever qualquer valor em 0xFB01 inicia a transmissão
  • Como a CPU não suporta interrupções, não há interrupções
  • Todos os endereços relacionados começam com F, com os 4 bits mais altos em 1, e essa condição é verificada por U2A
  • Os endereços de buffer exigem que o bit 11 seja 0, o que é verificado por U1D, D2, R2 e U1E
  • Os endereços de registrador exigem que o segundo dígito hexadecimal seja B, isto é, 1011, verificado por U1B e U2B
  • Os decodificadores U4A e U4B são usados para selecionar funções individuais
  • Dois LEDs indicam acesso ao buffer ou aos registradores

Programação e desempenho

  • Havia interesse em suporte de rede, mas não em implementar uma pilha TCP/IP do zero, e programar em assembly era incômodo, então foi criado um compilador C
  • Esse compilador amadureceu a ponto de conseguir compilar a pequena biblioteca TCP/IP uIP 1.0
  • A densidade de código da CPU é muito baixa, mas o uIP cabe na RAM e ainda sobra espaço para aplicações reais
  • O desempenho de rede é baixo, mas este é um resultado obtido sem CPU comercial nem chips especiais
    • média de ida e volta do ping: 85 ms
    • velocidade de download do servidor HTTP: 2.6 kB/s
    • o servidor HTTP serve arquivos estáticos de um cartão SD
  • Modelos, arquivos de esquemático e desenhos de PCB estão no repositório do GitHub

1 comentários

 
GN⁺ 2024-04-10
Comentários do Hacker News
  • Belo trabalho por compartilhar isso. Gostei especialmente do stack trace do processo de raciocínio, e é excelente do ponto de vista educacional como muita coisa foi resolvida a partir de primeiros princípios ou explicada sob a ótica de um iniciante
    Mesmo que seja pouco prático para redes reais, não acho que seja só uma brincadeira. Numa era em que backdoors são encontrados em chips de rede excessivamente complexos, talvez no futuro isso acabe atraindo um público mais sério ou motivações de projeto mais fortes

    • Fico pensando quantas vulnerabilidades escondidas dentro do silício moderno existem. Quase todo dia aparecem falhas em códigos com alguns milhares de linhas, então em silício hardcoded é como se houvesse microchips equivalentes a literalmente bilhões de linhas de código
  • Isso é para um computador totalmente customizado, o que por si só já é muito mais impressionante, e nem precisa falar da parte do “então eu fiz um compilador C”. Ainda assim, fiquei curioso sobre qual seria a implementação mínima de uma placa Ethernet para um PC “comum”
    Parece que boa parte seria parecida, e talvez os checksums pudessem ser processados pela CPU do PC. A conexão teria de ser serial bruta ou, de forma mais prática, USB, e no fim provavelmente teria de usar um driver “de verdade” ou passar isso para o espaço de usuário
    Ao ver coisas parecidas, pensei se não daria para fazer o dispositivo implementar https://en.wikipedia.org/wiki/USB_communications_device_clas... e assim “simplesmente funcionar” sem driver próprio, mas isso não parece combinar muito bem com deixar todos os checksums para o host
    Pesquisando, também encontrei https://en.wikipedia.org/wiki/Ethernet_over_USB e talvez isso signifique que seria possível fazer um adaptador que só converte a conexão física para USB e deixa o resto para o computador cuidar

    • Dá para argumentar que USB é muito mais complexo do que a antiga Ethernet 10base2. Se você tentar conectar uma rede Ethernet 10base2 por PCIe ou USB, ambos dão muito mais trabalho do que o lado Ethernet
      Talvez seja possível convencer um dispositivo USB no estilo FTDI a fazer bit banging de Ethernet 10base2. A ideia seria implementar só o lado “PHY”, convertendo o tráfego da linha em um fluxo de bits limpo e alinhando o início dos quadros, e deixar o PC fazer todo o resto em software
    • Se PCs comuns ainda tivessem um barramento ISA como há 30 anos, minha placa de rede poderia ser ligada nele com pouquíssimas modificações
    • Implementar uma NIC em FPGA é algo bem comum, normalmente junto com uma conexão PCIe
      Do lado USB, o CDC-NCM em si não é difícil de implementar em qualquer MCU, mas implementar um PHY USB HS na prática exige hardware ASIC
      Com um PHY ULPI USB HS de US$ 0,30, daria para implementar USB CDC-NCM em FPGA com bastante facilidade
  • No fim há um link para o compilador C criado para este projeto: https://github.com/imihajlow/ccpu-cc
    Parece que também tem linker e libc. Não entendo de verdade o quão complexo é o projeto de hardware, mas é impressionante que ele também tenha colocado um compilador C nisso quase como algo casual

    • É um compilador C escrito em Rust e usa o crate lang_c para o parsing da linguagem
  • Muito impressionante. Dá vontade de fazer um projeto assim eu mesmo, e fico admirado com a paixão e as incontáveis horas necessárias para entender o sistema e depois construir tudo
    Não estou exatamente ansioso para me aposentar, mas talvez seja nessa fase que eu acabe dedicando tempo a esse tipo de projeto de hardware e software

  • Então isso é melhor ou pior que uma Etherlink 3c501? :-D
    https://mirror.math.princeton.edu/pub/oldlinux/Linux.old/net...
    Se bem me lembro, era algo como pacotes novos vindos da rede sobrescreverem o buffer que a CPU estava tentando ler. Usei isso no Linux por um tempo e o desempenho era realmente péssimo

  • Achei interessante a parte que diz que “fixar o comprimento do quadro não afeta os protocolos de camada superior, porque eles codificam o tamanho do pacote no cabeçalho e não dependem do comprimento real do quadro Ethernet”
    Recentemente fiz um decodificador de pacotes e validei explicitamente em cada camada se o comprimento da camada inferior batia. No caso de IP, no meu decodificador o comprimento do datagrama IP precisa corresponder exatamente ao comprimento do quadro Ethernet e ao tamanho do cabeçalho da camada de enlace
    Não foi por capricho; eu queria detectar quadros curtos, e depois decidi tratar quadros longos também como erro. O autor usa uIP, mas fiquei curioso sobre como Linux e outros sistemas modernos lidam com isso. Também fiquei pensando se ele fez testes de interoperabilidade

    • Estou enviando quadros longos pela rede, e nenhum dos sistemas operacionais que tenho mostrou qualquer problema. Li em algum lugar que alguns roteadores realmente usam quadros longos para armazenar metadados no fim dos pacotes
    • Timestamps e outras formas de telemetria de rede in-band às vezes são inseridos no quadro como trailers. Nesse caso, um novo FCS é adicionado
      Se a aplicação não olha para os dados de L2, a pilha IP do Linux simplesmente ignora isso
  • Fisicamente, é muito menor que o primeiro conjunto de placas Ethernet SSI da DEC: https://i.ebayimg.com/images/g/NEYAAOSw-mZlg0lZ/s-l1600.jpg
    As placas DEC DEUNA têm mais de 30 cm de comprimento, mas também oferecem muito mais funcionalidades. A DEUNA é uma NIC “de verdade”: tem filas de transmissão e recepção, lida com isso de forma autônoma e também faz DMA. Claro, ela ainda roda seu próprio PDP-11 na placa para fazer tudo isso

  • Muito legal. Fico curioso para saber quanto tempo levou

    • Levei cerca de um mês para fazer o módulo de rede, mas escrever o compilador consumiu muito mais tempo
  • Isso mostra como é fácil colocar um backdoor dentro de um chip conectado à porta de rede

  • Em um curso de Communication Systems Engineering implementei o processamento de sinais de Ethernet e depois implementei uma pilha TCP/IP com ARP e switching em assembly QUIC para Motorola 68k
    Foram os 18 meses mais longos da minha vida