3 ポイント 投稿者 GN⁺ 2024-04-10 | 1件のコメント | WhatsAppで共有
  • 市販CPUや専用ネットワークチップを使わずに作ったディスクリートロジックコンピュータに、10BASE-T Ethernetの送受信機能を追加したハードウェア製作記
  • 以前に作成した10BASE-T↔SPI物理層アダプタの上にMAC層モジュールを追加してhomebrew computerに接続し、送信機と受信機は独立したフルデュプレックス構成になっている
  • 受信機はSPIデータをバイトに変換して2 kB SRAMに保存し、先頭6バイトをハードウェアで検査してFE:FA:F6:F2:EE:EAまたはブロードキャストMACだけを受け取る
  • 送信機は回路を単純化するため、FCS生成とプリアンブル準備をソフトウェアに任せ、1024バイト固定長フレームのみをサポートする
  • uIP 1.0をコンパイルできるCコンパイラまで作成してネットワークアプリを実行し、結果はping平均85 ms、HTTP静的ファイルのダウンロード2.6 kB/s程度

ディスクリートロジックコンピュータにEthernetを付ける

  • 完全なコンピュータシステムをディスクリートロジック部品で作る作業の延長として、ネットワークアプリケーションを実行できるEthernetアダプタを実装した
  • 以前は10BASE-T Ethernet信号をSPIに変換して戻す物理層アダプタを作成し、その際の動作テストにはSTM32マイクロコントローラを使用した
  • 今回の作業の核心は、そのアダプタをhomebrew computerに接続するためのMAC層モジュールである
  • アダプタはフルデュプレックス構造で、送信部と受信部が互いに独立して動作する

受信機: SPIデータをフレームバッファに保存

  • 受信機はSPIシリアルデータをバイト単位のパラレルデータに変換し、バイトクロックを抽出する
  • 宛先MACアドレスは先頭6バイトで検査し、条件に合わないフレームは拒否される
  • 受信したバイトは6116 2 kB SRAMバッファに書き込まれる
  • フレームが終わると受信機は無効化され、再度有効化されるまで追加フレームは受け取らない
  • バイトカウンタは停止後も値を保持し、CPUが受信長を読める
  • FCSはハードウェアでは検査しない
  • データ取得とバッファアクセス

    • SPIシリアルデータはシフトレジスタU32に入り、U30U31がそれぞれビット数とバイト数を数える
    • DフリップフロップU29BがSRAM書き込み信号recv_buf_weを生成し、この信号は入力データ8ビットごとに一瞬Lowになる
    • 受信したバイトはU20である6116 SRAMに記録される
    • U13U16U18はアドレスマルチプレクサを構成し、SRAMアドレス入力としてバイトカウンタまたはシステムアドレスバスを選択する
    • U21は受信バイトをRAMへ渡す3状態バッファとして動作する
    • CPUが受信データと長さにアクセスできるよう、RAMとバイトカウンタはシステムデータバスに接続されている
    • U25は受信RAMをシステムデータバスに接続する
    • フレーム完了後、バイトカウンタ値はrecv_byte_cntバスに保持される
    • U26U27は特定アドレスへの読み出し要求があると、この値をシステムデータバスへ渡す
    • U27の残り半分は、受信機と送信機の状態を照会する2ビット読み取り専用ステータスレジスタを作る

ハードウェアMACアドレスフィルタリング

  • Ethernetトラフィックを解析すると、フレームは通常、短い遅延で区切られた3〜4個単位の小さなまとまりで入ってきて、同じまとまりの中でも宛先MACアドレスが異なることが多かった
  • コンピュータがソフトウェアでMACフィルタリングを行って受信機を再有効化するには十分速くない可能性があり、ハードウェアMACフィルタリングが必要だった
  • ユーザー定義のMACアドレスを保存して先頭6バイトと比較する方式は複雑すぎるため除外した
  • 単一バイト反復MACアドレスも可能だったが、最終的にはバイトインデックスの関数としてMACアドレスを作った
    • bit 0は0に固定
    • bit 1は1に固定
    • bit 2〜4はバイトインデックスの反転値
    • bit 5〜7は1に固定
  • この規則で作られたMACアドレスはFE:FA:F6:F2:EE:EA
  • ARP動作のため、ブロードキャストMAC FF:FF:FF:FF:FF:FFも受け取る
  • U33はデータbit 0とbit 2〜4が期待値と一致するかを比較し、U34A出力はそれらのビットが一致するとHighになる
  • U35AはブロードキャストMAC検査を実装し、bit 0とbit 2〜4がすべて1なら出力がHighになる
  • この2つの信号はD7R6を使ったダイオードORで結合され、U35Bが残りのビットがすべて1かを検査する
  • 単一バイトの有効性結果はU10Aに累積される
    • フレームを受信していないとき、incoming SPI slave select信号ssはLowで、U10Aは1に設定される
    • フレーム受信中は、各受信バイトごとに値が更新される
    • 宛先MACアドレスが条件に合えば、U10Aの値はHighのまま維持される
    • バイトアドレスが5に達すると最終値がU36Bにラッチされ、宛先アドレスが一致しない場合はフレーム受信が阻止される

送信機: 固定長フレームで回路を単純化

  • 送信機も受信機と同様にFCS生成をハードウェアでは実装せず、ソフトウェアで処理する
  • 回路を減らすため、送信機は固定長フレームのみをサポートする
  • フレーム長は1024バイトに選び、一般的なMTU 1500バイトに近い値にした
  • 10BASE-Tに必要なプリアンブルは複数の0x55と末尾の0xD5で構成され、ソフトウェアがこの1024バイト内に一緒にロードしなければならない
  • 固定フレーム長は上位プロトコルに影響しない
    • 上位プロトコルはパケットサイズをヘッダにエンコードする
    • 実際のEthernetフレーム長には依存しない
  • 送信データフロー

    • 送信データはSRAMに保存される
    • 20 MHzクロックが4ビットカウンタに入力され、オーバーフロー出力がバイトクロックとして使われる
    • 特定の書き込み専用メモリ位置に値を書き込むとカウンタが有効化され、フレーム送信が始まる
    • パラレルのバイトデータはシフトレジスタを通してシリアル化される
    • 受信機と同様にU12がビットを数え、U14がバイトを数える
    • 20 MHzクロックは内蔵オシレータから来るが、直接は使わず少なくとも2分周して使用する
    • この方式により、オシレータのデューティサイクルが出力信号に影響しない
  • RAM、シフトレジスタ、タイミング

    • RAM U22のアドレス入力選択には、受信機と同様に3個の74HC157マルチプレクサが使われる
    • U23はRAMにデータをロードするために使われる
    • U24は現在送信中のバイトの中間保存領域として機能する
    • バイトカウンタ74HC4040はリップルカウンタのため安定化が遅い
    • RAM出力がまだ有効でない間、U24が安定した出力を提供する
    • データはシフトレジスタU28に入り、ビット単位でシフトされる
    • RAMからシフトレジスタへ入るビット順を誤って接続したハードウェアバグがあり、ソフトウェアでビットを並べ替えて回避しなければならなかった
    • MOSISCKは良好な10BASE-T信号を作るため、正確に同期している必要がある
    • U11AU8Bがこの同期を処理する
    • tx_cnt0はビットカウンタのbit 0で、20 MHzを2分周した信号をクロックとして使用する
    • U11Aはこの信号に合わせて出力を切り替える
    • U8BU11Aが作る遅延に合わせるためクロックを遅延させる
    • Dラッチは単純なANDゲートより複雑で約5 ns大きな遅延があるため、より高速な74LV74Aを使用した
    • 74LV74Aはこのボードで唯一の高速系列チップである

CPUインターフェースとメモリマッピング

  • プログラマの観点では、Ethernetアダプタはメモリマップトインターフェースとして見える
  • 2つのフレームバッファは0xF000にマップされている
  • 読み取り専用レジスタは2つある
    • 0xFB00の8ビットステータスレジスタはRX_FULLTX_BUSYフラグを持つ
    • RX_FULLはフレーム受信完了状態を示す
    • TX_BUSYはフレーム送信中状態を示す
    • 0xFB02の16ビットレジスタは受信データ長を保持する
  • 書き込み動作は制御コマンドとして使われる
    • 0xFB00に任意の値を書き込むと受信機を再有効化する
    • 0xFB01に任意の値を書き込むと送信を開始する
  • CPUは割り込みをサポートしないため、割り込みはない
  • 関連アドレスはすべて上位4ビットが1のFで始まり、この条件をU2Aが検査する
  • バッファアドレスはbit 11が0でなければならず、U1DD2R2U1Eがこれを検査する
  • レジスタアドレスは2番目の16進数桁がB1011である必要があり、U1BU2Bが確認する
  • U4AU4Bデコーダは個別機能の選択に使われる
  • LED 2個はバッファまたはレジスタアクセスを表示する

プログラミングと性能

  • ネットワーク対応は欲しかったがTCP/IPスタックを自作したくはなく、アセンブリプログラミングも不便だったため、Cコンパイラを作成した
  • このコンパイラは小さなTCP/IPライブラリであるuIP 1.0をコンパイルできるほど成熟している
  • CPUのコード密度は非常に低いが、uIPはRAMに収まり、実際のアプリケーション用の空間も残る
  • ネットワーク性能は低いが、市販CPUや特殊チップなしで実装した結果である
    • ping往復平均: 85 ms
    • HTTPサーバーのダウンロード速度: 2.6 kB/s
    • HTTPサーバーはSDカード上の静的ファイルを提供する
  • モデル、回路図ファイル、PCB図面はGitHubリポジトリにある

1件のコメント

 
GN⁺ 2024-04-10
Hacker News のコメント
  • 共有してくれてありがとう、素晴らしい仕事です。推論過程のスタックトレースが特に良く、多くのことを第一原理から解きほぐしていたり、初心者の視点で説明しようとしていたりする点が、教育的にとても優れています
    実際のネットワーキングには実用的でなくても、単なるお遊びではないと思います。過度に複雑なネットワークチップからバックドアが見つかる時代なので、今後はもっと真剣な読者層やプロジェクトの動機が生まれるかもしれません

    • 現代のシリコン内部に隠れた脆弱性がどれほど多いのか気になります。数千行のコードでもほぼ毎日のように脆弱性が出てくるのに、ハードコードされたシリコンの中には、実質的に数十億行のコードに相当するマイクロチップが入っているようなものです
  • これは完全カスタムコンピューター用なので、それ自体でもはるかに印象的ですし、「それで C コンパイラを作った」というくだりは言うまでもありません。それでも「普通の」PC 向け Ethernet カードの最小実装がどの程度になるのか気になります
    かなりの部分は似たものになりそうで、チェックサムは PC の CPU に処理させることもできそうです。接続は生のシリアルか、より実用的には USB が必要で、結局は「本物の」ドライバを使うか、ユーザー空間に渡して処理する必要がありそうです
    似たようなものを見ていて、デバイスが https://en.wikipedia.org/wiki/USB_communications_device_clas... を実装すれば、独自ドライバなしで「そのまま動く」ようにできるのではないかと思いましたが、チェックサムをすべてホスト側で処理するという話とはあまり相性が良くなさそうです
    検索しているうちに https://en.wikipedia.org/wiki/Ethernet_over_USB も見つけましたが、物理接続だけを USB に変換するアダプタを作り、残りはコンピューターに任せて処理させられる、という意味なのかもしれません

    • USB は、ずっと昔に出た 10base2 Ethernet よりはるかに複雑だと言えます。10base2 Ethernet ネットワークに PCIe や USB で接続しようとするなら、どちらも Ethernet 側よりずっと多くの作業になります
      FTDI 風の USB デバイスに 10base2 Ethernet をビットバンギングさせるよう説得することはできるかもしれません。回線上のトラフィックをきれいなビットストリームに変換し、フレーム開始を合わせる「PHY」側だけを実装して、PC が残りをすべてソフトウェアで処理する方式です
    • 一般的な PC に 30 年前のような ISA バスがあったなら、私のネットワークカードは少し修正するだけでそこにつなげられたでしょう
    • FPGA で NIC を実装するのは、通常 PCIe 接続と合わせて非常によくあります
      USB 側は CDC-NCM 自体ならどんな MCU でも実装は難しくありませんが、USB HS PHY の実装には実質的に ASIC ハードウェアが必要です
      0.30 ドルの USB HS ULPI PHY を使えば、FPGA で USB CDC-NCM をかなり簡単に実装できるはずです
  • 末尾に、このプロジェクト用に作った C コンパイラへのリンクがあります: https://github.com/imihajlow/ccpu-cc
    リンカと libc もあるようです。ハードウェア設計がどれほど複雑なのかは正確には分かりませんが、C コンパイラをさっと作って組み合わせた点がすごいです

    • Rust で書かれた C コンパイラで、言語のパースには lang_c クレートを使っています
  • 本当に印象的です。こういうプロジェクトを自分でもやってみたいですし、システムを理解してから作るところまで注ぎ込まれた情熱と数え切れない時間に敬意を抱きます
    引退を特に望んでいるわけではありませんが、たぶんその頃にはこうしたハードウェア・ソフトウェアプロジェクトに時間を使うことになるのかもしれません

  • それで Etherlink 3c501 より良いのか、それとも悪いのか? :-D
    https://mirror.math.princeton.edu/pub/oldlinux/Linux.old/net...
    記憶が正しければ、CPU が読み出そうとしていたバッファを、ネットワークから入ってきた新しいパケットが上書きするような仕組みでした。しばらく Linux で使っていましたが、性能は本当にひどかったです

    • 3c590(https://github.com/torvalds/linux/blob/20cb38a7af88dc40095da...)にも、ひどい挙動があったと記憶しています。致命的なエラーを避けるために、PCI レイテンシ設定を 32 から 248 に変更する必要がありました
      ドライバやファームウェアのアップデートが、どれほど多くのものを視界から隠してくれているのか驚かされます
    • この代物についての説明を見つけました: https://www.os2museum.com/wp/emulating-etherlink/
      私のものはバッファが 2 つあるので、もっとましです :) それでも受信フレームは 1 つしか保持されません
  • 「フレーム長を固定しても上位プロトコルには影響しない。上位プロトコルはヘッダにパケットサイズをエンコードしており、実際の Ethernet フレーム長には依存しないからだ」という部分が興味深いです
    最近パケットデコーダを作ったのですが、各層で下位層の長さが合っているかを明示的に検証しました。IP の場合、私のデコーダでは IP データグラム長が Ethernet フレーム長とリンク層ヘッダ長に正確に一致していなければなりません
    厳密にしたかったというより、短いフレームを検出したかったのですが、その後、長いフレームもエラーと見なすことにしました。作者は uIP を使っていますが、Linux や他の現代的な OS がどう処理するのか気になります。相互運用性テストをしたのかも気になります

    • 長いフレームをネットワークへ送出していますが、手元のどの OS でも問題は見られませんでした。一部のルーターはパケットの後ろにメタデータを保存するために、実際に長いフレームを使うとどこかで読んだことがあります
    • タイムスタンプや他の形のインバンドネットワークテレメトリが、トレーラー形式でフレームに挿入されることもあります。このとき新しい FCS が付きます
      アプリケーションが L2 データを見ないなら、Linux の IP スタックは単に無視します
  • DEC の最初の SSI Ethernet カードセットよりは、物理的にはずっと小さいです: https://i.ebayimg.com/images/g/NEYAAOSw-mZlg0lZ/s-l1600.jpg
    DEC DEUNA のボードは長さが 1 フィートを超えますが、機能もずっと多いです。DEUNA は「本物の」NIC で、送受信キューを持ち、それを自律的に処理し、DMA も行います。もちろんカード上に専用の PDP-11 も載っていて、それを実行しています

  • 本当に素晴らしいです。どれくらいかかったのか気になります

    • ネットワークモジュールを作るのには約 1 か月かかりましたが、コンパイラ作成にははるかに多くの時間がかかりました
  • ネットワークポートにつながったチップの中にバックドアを仕込むのがどれほど簡単かを示しています

  • Communication Systems Engineering の授業で Ethernet 信号処理を実装し、続いて ARP とスイッチングを含む TCP/IP スタックを Motorola 68k QUIC アセンブリで実装しました
    人生で最も長い 18 か月でした