3 điểm bởi GN⁺ 2024-04-10 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đây là bản ghi quá trình chế tạo phần cứng bổ sung chức năng thu/phát 10BASE-T Ethernet cho một máy tính logic rời rạc được làm không dùng CPU thương mại hay chip mạng chuyên dụng
  • Trên nền bộ chuyển đổi tầng vật lý 10BASE-T↔SPI đã làm trước đó, tác giả thêm mô-đun tầng MAC để kết nối với homebrew computer; bộ phát và bộ thu được cấu thành theo kiến trúc full-duplex độc lập
  • Bộ thu chuyển dữ liệu SPI thành byte, lưu vào SRAM 2 kB, và kiểm tra 6 byte đầu bằng phần cứng để chỉ nhận FE:FA:F6:F2:EE:EA hoặc MAC broadcast
  • Để đơn giản hóa mạch, bộ phát giao việc tạo FCS và chuẩn bị preamble cho phần mềm, và chỉ hỗ trợ frame có độ dài cố định 1024 byte
  • Tác giả còn tạo cả trình biên dịch C có thể biên dịch uIP 1.0 để chạy ứng dụng mạng; kết quả đạt ping trung bình 85 ms và tải file tĩnh qua HTTP ở mức 2,6 kB/s

Gắn Ethernet vào máy tính logic rời rạc

  • Như phần mở rộng của dự án tạo một hệ thống máy tính hoàn chỉnh bằng linh kiện logic rời rạc, tác giả đã triển khai bộ chuyển đổi Ethernet có thể chạy các ứng dụng mạng
  • Trước đó, tác giả đã làm bộ chuyển đổi tầng vật lý chuyển tín hiệu 10BASE-T Ethernet sang SPI và ngược lại, khi đó dùng vi điều khiển STM32 để kiểm thử hoạt động
  • Trọng tâm của lần này là mô-đun tầng MAC để kết nối bộ chuyển đổi đó với homebrew computer
  • Bộ chuyển đổi có kiến trúc full-duplex, với phần phát và phần thu hoạt động độc lập với nhau

Bộ thu: lưu dữ liệu SPI vào frame buffer

  • Bộ thu chuyển dữ liệu nối tiếp SPI thành dữ liệu song song theo byte và trích xuất byte clock
  • Địa chỉ MAC đích được kiểm tra trong 6 byte đầu; các frame không khớp tiêu chí sẽ bị từ chối
  • Các byte nhận được được ghi vào buffer SRAM 6116 2 kB
  • Khi frame kết thúc, bộ thu bị vô hiệu hóa và không nhận thêm frame cho đến khi được kích hoạt lại
  • Bộ đếm byte vẫn giữ giá trị sau khi dừng, để CPU có thể đọc độ dài nhận được
  • FCS không được kiểm tra bằng phần cứng
  • Thu thập dữ liệu và truy cập buffer

    • Dữ liệu nối tiếp SPI đi vào shift register U32, còn U30U31 lần lượt đếm bit và byte
    • D flip-flop U29B tạo tín hiệu ghi SRAM recv_buf_we, tín hiệu này hạ xuống trong chốc lát sau mỗi 8 bit dữ liệu đầu vào
    • Byte nhận được được ghi vào SRAM 6116 là U20
    • U13, U16, U18 tạo thành bộ multiplex địa chỉ, chọn bộ đếm byte hoặc bus địa chỉ hệ thống làm đầu vào địa chỉ SRAM
    • U21 đóng vai trò buffer ba trạng thái chuyển byte nhận được sang RAM
    • RAM và bộ đếm byte được nối với bus dữ liệu hệ thống để CPU có thể truy cập dữ liệu nhận và độ dài
    • U25 nối RAM nhận với bus dữ liệu hệ thống
    • Sau khi frame hoàn tất, giá trị bộ đếm byte được giữ trên bus recv_byte_cnt
    • U26, U27 đưa giá trị này lên bus dữ liệu hệ thống khi có yêu cầu đọc một địa chỉ cụ thể
    • Nửa còn lại của U27 tạo một thanh ghi trạng thái chỉ đọc 2 bit để truy vấn trạng thái bộ thu và bộ phát

Lọc địa chỉ MAC bằng phần cứng

  • Khi phân tích lưu lượng Ethernet, tác giả thấy frame thường đi vào theo các cụm nhỏ gồm 3~4 frame, được phân tách bởi độ trễ ngắn; ngay cả trong cùng một cụm, địa chỉ MAC đích cũng thường khác nhau
  • Máy tính có thể không đủ nhanh để thực hiện lọc MAC bằng phần mềm rồi kích hoạt lại bộ thu, nên cần lọc MAC bằng phần cứng
  • Cách lưu địa chỉ MAC tùy chỉnh rồi so sánh với 6 byte đầu bị loại vì quá phức tạp
  • Địa chỉ MAC lặp lại một byte duy nhất cũng khả thi, nhưng cuối cùng tác giả tạo địa chỉ MAC dưới dạng hàm của chỉ số byte
    • bit 0 cố định là 0
    • bit 1 cố định là 1
    • bit 2~4 là giá trị đảo của chỉ số byte
    • bit 5~7 cố định là 1
  • Địa chỉ MAC được tạo theo quy tắc này là FE:FA:F6:F2:EE:EA
  • Để ARP hoạt động, MAC broadcast FF:FF:FF:FF:FF:FF cũng được chấp nhận
  • U33 so sánh data bit 0 và bit 2~4 với giá trị mong muốn, và đầu ra U34A lên mức cao khi các bit đó khớp
  • U35A triển khai kiểm tra MAC broadcast; nếu bit 0 và bit 2~4 đều là 1 thì đầu ra lên mức cao
  • Hai tín hiệu được kết hợp bằng diode OR dùng D7R6, rồi U35B kiểm tra xem các bit còn lại có đều là 1 hay không
  • Kết quả hợp lệ của từng byte được tích lũy trong U10A
    • Khi không nhận frame, tín hiệu incoming SPI slave select ss ở mức thấp và U10A được đặt thành 1
    • Trong lúc nhận frame, giá trị được cập nhật ở mỗi byte nhận được
    • Nếu địa chỉ MAC đích khớp tiêu chí, giá trị U10A vẫn được giữ ở mức cao
    • Khi địa chỉ byte đạt 5, giá trị cuối cùng được chốt vào U36B; nếu địa chỉ đích không khớp thì việc nhận frame bị chặn

Bộ phát: đơn giản hóa mạch bằng frame độ dài cố định

  • Tương tự bộ thu, bộ phát cũng không triển khai tạo FCS bằng phần cứng mà xử lý bằng phần mềm
  • Để giảm mạch, bộ phát chỉ hỗ trợ frame có độ dài cố định
  • Độ dài frame được chọn là 1024 byte, một giá trị gần với MTU phổ biến 1500 byte
  • Preamble cần cho 10BASE-T gồm nhiều byte 0x55 và byte kết thúc 0xD5; phần mềm phải nạp chúng cùng trong 1024 byte này
  • Độ dài frame cố định không ảnh hưởng đến các giao thức tầng trên
    • Các giao thức tầng trên mã hóa kích thước gói trong header
    • Chúng không phụ thuộc vào độ dài frame Ethernet thực tế
  • Luồng dữ liệu phát

    • Dữ liệu phát được lưu trong SRAM
    • Clock 20 MHz được đưa vào bộ đếm 4 bit, và đầu ra overflow được dùng làm byte clock
    • Khi ghi một giá trị vào một vị trí bộ nhớ chỉ ghi cụ thể, bộ đếm được kích hoạt và bắt đầu phát frame
    • Dữ liệu byte song song được tuần tự hóa qua shift register
    • Giống bộ thu, U12 đếm bit và U14 đếm byte
    • Clock 20 MHz đến từ oscillator tích hợp và không được dùng trực tiếp, mà tối thiểu được chia 2 trước khi sử dụng
    • Cách này giúp duty cycle của oscillator không ảnh hưởng đến tín hiệu đầu ra
  • RAM, shift register và timing

    • Phần chọn đầu vào địa chỉ của RAM U22 dùng ba bộ multiplex 74HC157, giống như bộ thu
    • U23 được dùng để nạp dữ liệu vào RAM
    • U24 đóng vai trò bộ lưu tạm trung gian cho byte đang được phát hiện tại
    • Bộ đếm byte 74HC4040 là ripple counter nên ổn định chậm
    • Trong khi đầu ra RAM chưa hợp lệ, U24 cung cấp đầu ra ổn định
    • Dữ liệu đi vào shift register U28 và dịch theo từng bit
    • Có một lỗi phần cứng nối sai thứ tự bit từ RAM sang shift register, nên phần mềm phải trộn bit để обход qua
    • MOSISCK phải được đồng bộ chính xác để tạo tín hiệu 10BASE-T tốt
    • U11AU8B xử lý việc đồng bộ này
    • tx_cnt0 là bit 0 của bộ đếm bit, được dùng làm clock như tín hiệu 20 MHz chia 2
    • U11A thay đổi đầu ra theo tín hiệu này
    • U8B trì hoãn clock để khớp với độ trễ do U11A tạo ra
    • D latch phức tạp hơn cổng AND đơn giản và có độ trễ lớn hơn khoảng 5 ns, nên tác giả dùng 74LV74A nhanh hơn
    • 74LV74A là chip duy nhất thuộc dòng tốc độ cao trên bo mạch này

Giao diện CPU và ánh xạ bộ nhớ

  • Từ góc nhìn lập trình viên, bộ chuyển đổi Ethernet hiện ra như một giao diện memory-mapped
  • Hai frame buffer được ánh xạ tại 0xF000
  • Có hai thanh ghi chỉ đọc
    • Thanh ghi trạng thái 8 bit tại 0xFB00 có các cờ RX_FULLTX_BUSY
    • RX_FULL biểu thị trạng thái nhận frame hoàn tất
    • TX_BUSY biểu thị trạng thái đang phát frame
    • Thanh ghi 16 bit tại 0xFB02 chứa độ dài dữ liệu nhận được
  • Thao tác ghi được dùng như lệnh điều khiển
    • Ghi bất kỳ giá trị nào vào 0xFB00 sẽ kích hoạt lại bộ thu
    • Ghi bất kỳ giá trị nào vào 0xFB01 sẽ bắt đầu phát
  • Do CPU không hỗ trợ interrupt nên không có interrupt
  • Tất cả địa chỉ liên quan đều bắt đầu bằng F, tức 4 bit cao đều là 1; điều kiện này được U2A kiểm tra
  • Địa chỉ buffer phải có bit 11 là 0; U1D, D2, R2, U1E kiểm tra điều này
  • Địa chỉ thanh ghi phải có chữ số thập lục phân thứ hai là B, tức 1011; U1BU2B xác nhận điều này
  • Các decoder U4A, U4B được dùng để chọn từng chức năng riêng lẻ
  • Hai đèn LED hiển thị truy cập buffer hoặc thanh ghi

Lập trình và hiệu năng

  • Tác giả muốn có hỗ trợ mạng nhưng không muốn tự triển khai TCP/IP stack, và việc lập trình assembly cũng bất tiện, nên đã tạo trình biên dịch C
  • Trình biên dịch này đã đủ trưởng thành để biên dịch uIP 1.0, một thư viện TCP/IP nhỏ
  • Mật độ mã của CPU rất thấp, nhưng uIP vẫn nằm vừa trong RAM và còn chừa chỗ cho ứng dụng thực tế
  • Hiệu năng mạng thấp, nhưng đây là kết quả đạt được mà không dùng CPU thương mại hay chip chuyên dụng
    • Thời gian khứ hồi ping trung bình: 85 ms
    • Tốc độ tải xuống từ HTTP server: 2,6 kB/s
    • HTTP server cung cấp các file tĩnh trên thẻ SD
  • Model, file sơ đồ mạch và bản vẽ PCB có trong kho GitHub

1 bình luận

 
GN⁺ 2024-04-10
Ý kiến từ Hacker News
  • Thật tuyệt khi bạn chia sẻ công trình này. Mình đặc biệt thích stack trace của quá trình suy luận, và việc bạn giải quyết nhiều thứ từ các nguyên lý đầu tiên hoặc cố gắng giải thích từ góc nhìn của người mới bắt đầu là cực kỳ có giá trị về mặt giáo dục.
    Dù không thực dụng cho mạng máy tính thực tế, mình không nghĩ đây chỉ là một trò đùa đơn thuần. Trong thời đại mà các backdoor bị phát hiện trong những chip mạng phức tạp quá mức, sau này có thể sẽ xuất hiện một lượng độc giả nghiêm túc hơn hoặc động lực dự án rõ ràng hơn

    • Mình tự hỏi có bao nhiêu lỗ hổng ẩn bên trong silicon hiện đại. Với vài nghìn dòng mã mà lỗ hổng gần như ngày nào cũng xuất hiện, thì trong silicon được hardcode thực chất lại có những vi mạch tương đương hàng tỷ dòng mã
  • Đây là thứ dành cho một máy tính hoàn toàn tự chế, nên bản thân nó đã ấn tượng hơn nhiều, chưa kể đến đoạn “thế là tôi làm luôn một trình biên dịch C”. Dù vậy, mình vẫn tò mò mức triển khai tối thiểu của một card Ethernet cho PC “thông thường” sẽ đến đâu.
    Có lẽ phần lớn sẽ tương tự, và checksum cũng có thể để CPU của PC xử lý. Kết nối thì sẽ cần serial thô, hoặc thực tế hơn là USB, và rốt cuộc có lẽ phải dùng driver “thật” hoặc đẩy sang user space để xử lý.
    Khi xem những thứ tương tự, mình từng nghĩ nếu thiết bị triển khai https://en.wikipedia.org/wiki/USB_communications_device_clas... thì có thể làm cho nó “cứ thế mà chạy” mà không cần driver riêng, nhưng có vẻ điều đó không hợp lắm với việc để mọi checksum do phía host xử lý.
    Trong lúc tìm kiếm mình còn thấy https://en.wikipedia.org/wiki/Ethernet_over_USB, nên có lẽ điều đó có nghĩa là có thể tạo một adapter chỉ chuyển đổi kết nối vật lý sang USB rồi để máy tính tự lo phần còn lại

    • Có thể xem USB là phức tạp hơn rất nhiều so với 10base2 Ethernet từ rất lâu trước đây. Nếu định nối vào một mạng Ethernet 10base2 qua PCIe hoặc USB, thì cả hai đều sẽ là lượng công việc lớn hơn rất nhiều so với phía Ethernet.
      Có lẽ có thể thuyết phục một thiết bị USB kiểu FTDI bit-bang 10base2 Ethernet. Cách làm là chỉ triển khai phần “PHY” để biến lưu lượng trên đường truyền thành một bitstream sạch và đồng bộ điểm bắt đầu khung, rồi để PC xử lý toàn bộ phần còn lại bằng phần mềm
    • Nếu PC phổ thông vẫn còn bus ISA như 30 năm trước, thì card mạng của mình chỉ cần chỉnh sửa một chút là có thể cắm vào đó
    • Việc triển khai NIC bằng FPGA thực ra khá phổ biến, thường đi kèm kết nối PCIe.
      Phía USB thì bản thân CDC-NCM không khó triển khai trên bất kỳ MCU nào, nhưng việc triển khai USB HS PHY trên thực tế gần như cần phần cứng ASIC.
      Dùng một USB HS ULPI PHY giá 0,30 USD thì có thể triển khai USB CDC-NCM trên FPGA khá dễ dàng
  • Ở cuối bài có liên kết đến trình biên dịch C được tạo cho dự án này: https://github.com/imihajlow/ccpu-cc
    Có vẻ cũng có cả linker và libc. Mình không thật sự biết thiết kế phần cứng phức tạp đến mức nào, nhưng việc tiện tay làm luôn một trình biên dịch C để gắn vào là điều rất đáng nể

    • Đây là trình biên dịch C viết bằng Rust, và dùng crate lang_c để phân tích cú pháp ngôn ngữ
  • Thật sự rất ấn tượng. Mình muốn tự làm một dự án như thế này, và rất kính nể niềm đam mê cùng vô số thời gian cần bỏ ra để hiểu hệ thống rồi đi đến mức tự chế tạo được nó.
    Mình không hẳn mong đến lúc nghỉ hưu, nhưng có lẽ khi đó mình sẽ dành thời gian cho những dự án phần cứng/phần mềm kiểu này

  • Vậy nó tốt hơn hay tệ hơn Etherlink 3c501? :-D
    https://mirror.math.princeton.edu/pub/oldlinux/Linux.old/net...
    Nếu mình nhớ không nhầm, kiểu như gói tin mới đi vào từ mạng sẽ ghi đè lên buffer mà CPU đang định đọc. Mình đã dùng nó với Linux một thời gian, và hiệu năng thật sự rất tệ

  • Đoạn “ngay cả khi cố định độ dài frame thì cũng không ảnh hưởng đến các giao thức tầng trên, vì các giao thức tầng trên mã hóa kích thước gói trong header và không phụ thuộc vào độ dài frame Ethernet thực tế” rất thú vị.
    Gần đây mình có làm một bộ giải mã gói tin, và đã xác minh tường minh ở từng tầng xem độ dài của tầng dưới có khớp không. Với IP, trong bộ giải mã của mình thì độ dài IP datagram phải khớp chính xác với độ dài frame Ethernet và độ dài header tầng liên kết.
    Không phải vì mình muốn quá khắt khe, mà là để phát hiện frame bị ngắn; sau đó mình cũng quyết định coi frame quá dài là lỗi. Tác giả dùng uIP, nhưng mình tò mò Linux hoặc các OS hiện đại khác xử lý chuyện này thế nào. Cũng tò mò không biết tác giả có kiểm thử khả năng tương tác hay không

    • Tôi đang phát các frame dài lên mạng, và chưa OS nào tôi có tỏ ra có vấn đề. Tôi từng đọc đâu đó rằng một số router thực sự dùng frame dài để lưu metadata ở cuối gói
    • Timestamp và các dạng in-band network telemetry khác đôi khi cũng được chèn vào frame dưới dạng trailer. Khi đó sẽ có FCS mới.
      Nếu ứng dụng không nhìn vào dữ liệu L2 thì Linux IP stack sẽ đơn giản là bỏ qua
  • Xét về mặt vật lý thì nó nhỏ hơn rất nhiều so với bộ card SSI Ethernet đầu tiên của DEC: https://i.ebayimg.com/images/g/NEYAAOSw-mZlg0lZ/s-l1600.jpg
    Các bo mạch DEC DEUNA dài hơn 1 foot, nhưng cũng có nhiều chức năng hơn hẳn. DEUNA là một NIC “thật sự”, có hàng đợi gửi/nhận và tự xử lý chúng, đồng thời còn làm DMA. Tất nhiên nó cũng chạy một PDP-11 riêng trên card

  • Quá tuyệt. Không biết mất bao lâu để làm xong

    • Mất khoảng một tháng để làm module mạng, nhưng viết trình biên dịch còn tốn nhiều thời gian hơn rất nhiều
  • Điều này cho thấy việc cài backdoor vào con chip nối với cổng mạng có thể dễ đến mức nào

  • Trong khóa học Communication Systems Engineering, mình đã triển khai xử lý tín hiệu Ethernet, rồi tiếp tục triển khai TCP/IP stack có cả ARP và switching bằng Motorola 68k QUIC assembly
    Đó là 18 tháng dài nhất trong cuộc đời mình