- เป็นบันทึกการสร้างฮาร์ดแวร์ที่เพิ่มความสามารถรับส่ง 10BASE-T Ethernet ให้กับ คอมพิวเตอร์ลอจิกแบบดิสครีต ที่สร้างขึ้นโดยไม่มี CPU เชิงพาณิชย์หรือชิปเครือข่ายเฉพาะทาง
- เพิ่มโมดูลชั้น MAC บน อะแดปเตอร์ชั้นกายภาพ 10BASE-T↔SPI ที่เคยสร้างไว้ก่อนหน้า เพื่อเชื่อมต่อกับ homebrew computer และจัดโครงสร้างตัวส่งกับตัวรับให้เป็นฟูลดูเพล็กซ์ที่ทำงานแยกกัน
- ตัวรับแปลงข้อมูล SPI เป็นไบต์แล้วเก็บใน SRAM 2 kB และตรวจสอบ 6 ไบต์แรกด้วยฮาร์ดแวร์ โดยรับเฉพาะ
FE:FA:F6:F2:EE:EAหรือ MAC แบบบรอดแคสต์เท่านั้น - เพื่อลดความซับซ้อนของวงจร ตัวส่งให้ ซอฟต์แวร์รับผิดชอบการสร้าง FCS และการเตรียมพรีแอมเบิล และรองรับเฉพาะเฟรมความยาวคงที่ 1024 ไบต์
- สร้างแม้กระทั่งคอมไพเลอร์ C ที่คอมไพล์ uIP 1.0 ได้เพื่อรันแอปเครือข่าย โดยผลลัพธ์อยู่ที่ ping เฉลี่ย 85 ms และดาวน์โหลดไฟล์สแตติกผ่าน HTTP ได้ 2.6 kB/s
ต่อ Ethernet เข้ากับคอมพิวเตอร์ลอจิกแบบดิสครีต
- ในฐานะงานต่อยอดจากการสร้างระบบคอมพิวเตอร์ทั้งชุดด้วย ชิ้นส่วนลอจิกแบบดิสครีต ได้พัฒนาอะแดปเตอร์ Ethernet ที่สามารถรันแอปพลิเคชันเครือข่ายได้
- ก่อนหน้านี้เคยสร้าง อะแดปเตอร์ชั้นกายภาพที่แปลงสัญญาณ 10BASE-T Ethernet เป็น SPI และแปลงกลับ และในตอนนั้นใช้ไมโครคอนโทรลเลอร์ STM32 สำหรับทดสอบการทำงาน
- หัวใจของงานครั้งนี้คือ โมดูลชั้น MAC สำหรับเชื่อมต่ออะแดปเตอร์ดังกล่าวเข้ากับ homebrew computer
- อะแดปเตอร์มีโครงสร้างแบบ ฟูลดูเพล็กซ์ โดยภาคส่งและภาครับทำงานอย่างเป็นอิสระจากกัน
ตัวรับ: บันทึกข้อมูล SPI ลงในเฟรมบัฟเฟอร์
- ตัวรับจะแปลงข้อมูลอนุกรม SPI เป็นข้อมูลขนานระดับไบต์ และแยก byte clock ออกมา
- ตรวจสอบที่อยู่ MAC ปลายทางจาก 6 ไบต์แรก และปฏิเสธเฟรมที่ไม่ตรงตามเกณฑ์
- ไบต์ที่รับได้จะถูกเขียนลงในบัฟเฟอร์ 6116 2 kB SRAM
- เมื่อเฟรมจบ ตัวรับจะถูกปิดใช้งาน และจะไม่รับเฟรมเพิ่มเติมจนกว่าจะถูกเปิดใช้งานอีกครั้ง
- ตัวนับไบต์ยังคงค่าหลังจากหยุด เพื่อให้ CPU อ่านความยาวที่รับได้
- FCS ไม่ได้ถูกตรวจสอบในฮาร์ดแวร์
-
การเก็บข้อมูลและการเข้าถึงบัฟเฟอร์
- ข้อมูลอนุกรม SPI เข้าสู่ชิฟต์รีจิสเตอร์
U32โดยU30และU31นับบิตและไบต์ตามลำดับ - D flip-flop
U29Bสร้างสัญญาณเขียน SRAMrecv_buf_weและสัญญาณนี้จะลดต่ำลงชั่วครู่ทุก ๆ ข้อมูลอินพุต 8 บิต - ไบต์ที่รับได้ถูกเขียนลงใน 6116 SRAM ซึ่งคือ
U20 U13,U16,U18ประกอบกันเป็น address multiplexer เพื่อเลือกตัวนับไบต์หรือ system address bus เป็นอินพุตที่อยู่ของ SRAMU21ทำหน้าที่เป็น บัฟเฟอร์สามสถานะ ที่ส่งไบต์ที่รับได้ไปยัง RAM- RAM และตัวนับไบต์เชื่อมต่อกับ system data bus เพื่อให้ CPU เข้าถึงข้อมูลที่รับและความยาวได้
U25เชื่อม RAM ฝั่งรับเข้ากับ system data bus- หลังเฟรมเสร็จ ค่าของตัวนับไบต์จะคงอยู่บนบัส
recv_byte_cnt U26,U27จะส่งค่านี้ไปยัง system data bus เมื่อมีคำขออ่านที่อยู่เฉพาะ- ครึ่งที่เหลือของ
U27สร้างรีจิสเตอร์สถานะแบบอ่านอย่างเดียว 2 บิตสำหรับตรวจสถานะตัวรับและตัวส่ง
- ข้อมูลอนุกรม SPI เข้าสู่ชิฟต์รีจิสเตอร์
การกรองที่อยู่ MAC ด้วยฮาร์ดแวร์
- เมื่อวิเคราะห์ทราฟฟิก Ethernet พบว่าเฟรมมักเข้ามาเป็น กลุ่มเล็ก ๆ ชุดละ 3~4 เฟรม คั่นด้วยดีเลย์สั้น ๆ และแม้ในกลุ่มเดียวกันก็มักมีที่อยู่ MAC ปลายทางต่างกัน
- คอมพิวเตอร์อาจไม่เร็วพอที่จะทำ MAC filtering ด้วยซอฟต์แวร์แล้วเปิดใช้งานตัวรับใหม่ จึงจำเป็นต้องมี MAC filtering ด้วยฮาร์ดแวร์
- วิธีเก็บที่อยู่ 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เปรียบเทียบว่า data bit 0 และ bit 2~4 ตรงกับค่าที่ต้องการหรือไม่ และเอาต์พุตU34Aจะเป็นสูงเมื่อบิตเหล่านั้นตรงกันU35Aใช้ตรวจ MAC แบบบรอดแคสต์ โดยเอาต์พุตจะเป็นสูงเมื่อ bit 0 และ bit 2~4 เป็น 1 ทั้งหมด- สัญญาณทั้งสองถูกรวมกันด้วย diode OR โดยใช้
D7และR6และU35Bตรวจว่าบิตที่เหลือทั้งหมดเป็น 1 หรือไม่ - ผลความถูกต้องของไบต์เดียวถูกสะสมใน
U10A- เมื่อไม่ได้รับเฟรม สัญญาณ incoming SPI slave select
ssจะต่ำ และU10Aถูกตั้งเป็น 1 - ระหว่างรับเฟรม ค่าจะถูกอัปเดตทุกไบต์ที่รับ
- หากที่อยู่ MAC ปลายทางตรงตามเกณฑ์ ค่า
U10Aจะคงเป็นสูง - เมื่อที่อยู่ไบต์ถึง 5 ค่าสุดท้ายจะถูก latch เข้า
U36Bและหากที่อยู่ปลายทางไม่ตรง การรับเฟรมจะถูกบล็อก
- เมื่อไม่ได้รับเฟรม สัญญาณ incoming SPI slave select
ตัวส่ง: ลดความซับซ้อนของวงจรด้วยเฟรมความยาวคงที่
- เช่นเดียวกับตัวรับ ตัวส่งไม่สร้าง FCS ในฮาร์ดแวร์ แต่ให้ซอฟต์แวร์จัดการ
- เพื่อลดวงจร ตัวส่งรองรับเฉพาะเฟรมความยาวคงที่
- เลือกความยาวเฟรมเป็น 1024 ไบต์ ซึ่งใกล้เคียงกับ MTU ทั่วไป 1500 ไบต์
- พรีแอมเบิลที่จำเป็นสำหรับ 10BASE-T ประกอบด้วย
0x55หลายตัวและ0xD5ที่ท้าย โดยซอฟต์แวร์ต้องโหลดรวมไว้ใน 1024 ไบต์นี้ด้วย - ความยาวเฟรมคงที่ไม่กระทบโปรโตคอลชั้นบน
- โปรโตคอลชั้นบนเข้ารหัสขนาดแพ็กเก็ตไว้ใน header
- ไม่พึ่งพาความยาวเฟรม Ethernet จริง
-
เส้นทางข้อมูลส่ง
- ข้อมูลส่งถูกเก็บไว้ใน SRAM
- clock 20 MHz ถูกป้อนเข้าตัวนับ 4 บิต และใช้เอาต์พุต overflow เป็น byte clock
- เมื่อเขียนค่าใด ๆ ไปยังตำแหน่งหน่วยความจำแบบเขียนอย่างเดียวที่กำหนด ตัวนับจะเริ่มทำงานและเริ่มส่งเฟรม
- ข้อมูลไบต์แบบขนานถูก serialize ผ่านชิฟต์รีจิสเตอร์
- เช่นเดียวกับตัวรับ
U12นับบิตและU14นับไบต์ - clock 20 MHz มาจากออสซิลเลเตอร์ในตัว และไม่ได้ใช้โดยตรง แต่แบ่งความถี่อย่างน้อย 2 เท่าก่อนใช้งาน
- วิธีนี้ทำให้ duty cycle ของออสซิลเลเตอร์ไม่ส่งผลต่อสัญญาณเอาต์พุต
-
RAM, ชิฟต์รีจิสเตอร์ และไทมิง
- การเลือกอินพุตที่อยู่ของ RAM
U22ใช้มัลติเพล็กเซอร์ 74HC157 จำนวนสามตัวเช่นเดียวกับตัวรับ U23ใช้สำหรับโหลดข้อมูลเข้า RAMU24ทำหน้าที่เป็น ที่พักข้อมูลชั่วคราว ของไบต์ที่กำลังส่งอยู่- ตัวนับไบต์ 74HC4040 เป็น ripple counter จึงเสถียรช้า
U24ให้เอาต์พุตที่เสถียรในช่วงที่เอาต์พุต RAM ยังไม่ valid- ข้อมูลเข้าสู่ชิฟต์รีจิสเตอร์
U28แล้วเลื่อนทีละบิต - มีบั๊กฮาร์ดแวร์ที่ต่อเรียงลำดับบิตจาก RAM ไปยังชิฟต์รีจิสเตอร์ผิด จึงต้องให้ซอฟต์แวร์สลับบิตเพื่อเลี่ยงปัญหา
MOSIและSCKต้องซิงโครไนซ์อย่างแม่นยำเพื่อสร้างสัญญาณ 10BASE-T ที่ดีU11AและU8Bจัดการการซิงโครไนซ์นี้tx_cnt0คือ bit 0 ของตัวนับบิต และใช้เป็น clock ด้วยสัญญาณที่ได้จากการหาร 20 MHz ด้วย 2U11Aเปลี่ยนเอาต์พุตตามสัญญาณนี้U8Bหน่วง clock เพื่อให้สอดคล้องกับดีเลย์ที่U11Aสร้างขึ้น- D latch ซับซ้อนกว่า AND gate แบบง่ายและมีดีเลย์มากกว่าประมาณ 5 ns จึงใช้ 74LV74A ที่เร็วกว่า
- 74LV74A เป็นชิปตระกูลความเร็วสูงตัวเดียวบนบอร์ดนี้
- การเลือกอินพุตที่อยู่ของ RAM
อินเทอร์เฟซ CPU และการแมปหน่วยความจำ
- จากมุมมองของโปรแกรมเมอร์ อะแดปเตอร์ Ethernet ปรากฏเป็นอินเทอร์เฟซแบบ memory-mapped
- เฟรมบัฟเฟอร์ทั้งสองถูกแมปที่
0xF000 - มีรีจิสเตอร์อ่านอย่างเดียวสองตัว
- รีจิสเตอร์สถานะ 8 บิตที่
0xFB00มีแฟล็กRX_FULLและTX_BUSY RX_FULLแสดงสถานะรับเฟรมเสร็จTX_BUSYแสดงสถานะกำลังส่งเฟรม- รีจิสเตอร์ 16 บิตที่
0xFB02เก็บความยาวข้อมูลที่รับได้
- รีจิสเตอร์สถานะ 8 บิตที่
- การเขียนถูกใช้เป็นคำสั่งควบคุม
- เขียนค่าใด ๆ ไปที่
0xFB00เพื่อเปิดใช้งานตัวรับอีกครั้ง - เขียนค่าใด ๆ ไปที่
0xFB01เพื่อเริ่มส่ง
- เขียนค่าใด ๆ ไปที่
- เนื่องจาก CPU ไม่รองรับ interrupt จึง ไม่มี interrupt
- ที่อยู่ที่เกี่ยวข้องทั้งหมดเริ่มด้วย
Fซึ่งมี 4 บิตบนเป็น 1 และU2Aตรวจเงื่อนไขนี้ - ที่อยู่บัฟเฟอร์ต้องมี bit 11 เป็น 0 โดย
U1D,D2,R2,U1Eตรวจเงื่อนไขนี้ - ที่อยู่รีจิสเตอร์ต้องมีเลขฐานสิบหกหลักที่สองเป็น
Bหรือ1011โดยU1BและU2Bตรวจสอบ - ดีโคเดอร์
U4A,U4Bใช้เลือกฟังก์ชันแต่ละอย่าง - LED สองดวงแสดงการเข้าถึงบัฟเฟอร์หรือรีจิสเตอร์
การเขียนโปรแกรมและประสิทธิภาพ
- ต้องการการรองรับเครือข่าย แต่ไม่อยากสร้าง TCP/IP stack เอง และการเขียน assembly ก็ไม่สะดวก จึงสร้าง คอมไพเลอร์ C
- คอมไพเลอร์นี้สมบูรณ์พอที่จะคอมไพล์ uIP 1.0 ซึ่งเป็นไลบรารี TCP/IP ขนาดเล็กได้
- code density ของ CPU ต่ำมาก แต่ uIP ใส่ลงใน RAM ได้ และยังเหลือพื้นที่สำหรับแอปพลิเคชันจริงด้วย
- ประสิทธิภาพเครือข่ายต่ำ แต่เป็นผลลัพธ์ที่ได้โดยไม่ใช้ CPU เชิงพาณิชย์หรือชิปพิเศษ
- ค่าเฉลี่ยรอบไปกลับของ ping: 85 ms
- ความเร็วดาวน์โหลดของ HTTP server: 2.6 kB/s
- HTTP server ให้บริการไฟล์สแตติกจาก SD card
- โมเดล ไฟล์แผนผังวงจร และแบบ PCB อยู่ใน GitHub repository
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
งานเจ๋งมาก ขอบคุณที่แชร์ โดยเฉพาะ stack trace ของกระบวนการให้เหตุผล นั้นดีมาก และการที่อธิบายหลายอย่างจากหลักการพื้นฐาน หรือพยายามอธิบายในมุมมองของมือใหม่ ก็มีคุณค่าทางการศึกษามาก
แม้จะไม่ค่อยใช้งานได้จริงกับงานเครือข่ายจริง ๆ แต่ผมไม่คิดว่านี่เป็นแค่ของเล่นธรรมดา ในยุคที่พบ backdoor ในชิปเครือข่ายที่ซับซ้อนเกินไป อนาคตอาจมีผู้อ่านที่จริงจังขึ้น หรือมีแรงจูงใจของโปรเจกต์แบบนี้มากขึ้นก็ได้
นี่ทำขึ้นสำหรับคอมพิวเตอร์แบบ custom ล้วน ๆ เลยยิ่งน่าประทับใจกว่าเดิม และไม่ต้องพูดถึงตอนที่บอกว่า “ก็เลยทำ C compiler ขึ้นมา” ถึงอย่างนั้นก็ทำให้อยากรู้ว่า การทำ Ethernet card สำหรับ PC “ทั่วไป” แบบขั้นต่ำสุด จะต้องมีประมาณไหน
หลายส่วนน่าจะคล้ายกัน และ checksum ก็น่าจะให้ CPU ของ PC จัดการได้ ส่วนการเชื่อมต่อคงต้องเป็น serial ดิบ ๆ หรือถ้าให้ใช้งานได้จริงกว่าก็ต้องเป็น USB และสุดท้ายก็คงต้องใช้ไดรเวอร์ “จริง ๆ” หรือส่งต่อไปจัดการใน user space
จากที่ดูของคล้าย ๆ กัน ผมเคยคิดว่าถ้าอุปกรณ์ implement https://en.wikipedia.org/wiki/USB_communications_device_clas... ได้ ก็น่าจะทำให้มัน “ใช้ได้เลย” โดยไม่ต้องมีไดรเวอร์ของตัวเอง แต่ดูจะไม่ค่อยเข้ากับการให้ host จัดการ checksum ทั้งหมดเท่าไร
ระหว่างค้นหาก็เจอ https://en.wikipedia.org/wiki/Ethernet_over_USB ด้วย ซึ่งอาจหมายความว่าสามารถทำอะแดปเตอร์ที่แปลงเฉพาะการเชื่อมต่อทางกายภาพให้เป็น USB แล้วปล่อยให้คอมพิวเตอร์จัดการส่วนที่เหลือเองได้ก็ได้
อาจพอเกลี้ยกล่อมให้อุปกรณ์ USB สไตล์ FTDI bit-bang 10base2 Ethernet ได้อยู่ วิธีคือ implement แค่ฝั่ง “PHY” ที่แปลงทราฟฟิกบนสายให้เป็น bitstream สะอาด ๆ และจัดให้ตรงกับจุดเริ่มต้นของเฟรม แล้วให้ PC จัดการส่วนที่เหลือทั้งหมดด้วยซอฟต์แวร์
ฝั่ง USB นั้น CDC-NCM เอง implement บน MCU ตัวไหนก็ไม่ยาก แต่การ implement USB HS PHY แทบต้องใช้ฮาร์ดแวร์ ASIC
ถ้าใช้ USB HS ULPI PHY ราคา 0.30 ดอลลาร์ ก็น่าจะ implement USB CDC-NCM บน FPGA ได้ค่อนข้างง่าย
ตอนท้ายมีลิงก์ไปยัง C compiler ที่ทำขึ้นสำหรับโปรเจกต์นี้: https://github.com/imihajlow/ccpu-cc
ดูเหมือนจะมี linker กับ libc ด้วย ผมไม่รู้จริง ๆ ว่าดีไซน์ฮาร์ดแวร์ซับซ้อนแค่ไหน แต่การทำ C compiler แบบเบา ๆ แล้วเอามาประกอบด้วยนั้นสุดยอดมาก
น่าประทับใจจริง ๆ ผมอยากลองทำโปรเจกต์แบบนี้เอง และนับถือทั้งความทุ่มเทกับเวลานับไม่ถ้วนที่ใช้ไปกับการทำความเข้าใจระบบแล้วสร้างมันขึ้นมา
ผมไม่ได้อยากเกษียณเป็นพิเศษ แต่บางทีตอนนั้นผมอาจได้ใช้เวลากับ โปรเจกต์ฮาร์ดแวร์·ซอฟต์แวร์ แบบนี้ก็ได้
แล้วมันดีกว่า Etherlink 3c501 หรือแย่กว่ากันแน่? :-D
https://mirror.math.princeton.edu/pub/oldlinux/Linux.old/net...
ถ้าจำไม่ผิด มันเป็นแบบที่แพ็กเก็ตใหม่ที่เข้ามาจากเครือข่ายจะเขียนทับบัฟเฟอร์ที่ CPU กำลังจะอ่าน ผมเคยใช้บน Linux อยู่ช่วงหนึ่ง ประสิทธิภาพแย่มากจริง ๆ
น่าทึ่งมากว่าไดรเวอร์กับอัปเดตเฟิร์มแวร์ช่วยซ่อนอะไรจากสายตาเราไว้ได้มากแค่ไหน
ของผมดีกว่าเพราะมีบัฟเฟอร์สองตัว :) ถึงอย่างนั้นก็เก็บเฟรมขาเข้าได้แค่เฟรมเดียว
ส่วนที่ว่า “แม้จะกำหนดความยาวเฟรมคงที่ ก็ไม่มีผลต่อโปรโตคอลชั้นบน เพราะโปรโตคอลชั้นบน encode ขนาดแพ็กเก็ตไว้ใน header และไม่ได้พึ่งพาความยาวจริงของ Ethernet frame” น่าสนใจดี
ผมเพิ่งทำ packet decoder ไปไม่นาน และตรวจสอบอย่างชัดเจนในแต่ละชั้นว่าความยาวของชั้นล่างตรงกันหรือไม่ สำหรับ IP ใน decoder ของผม ความยาวของ IP datagram ต้องตรงพอดีกับความยาว Ethernet frame และความยาว header ของ link layer
ไม่ใช่ว่าผมตั้งใจจะละเอียดอะไร แค่ต้องการตรวจจับเฟรมที่สั้นเกินไป แล้วหลังจากนั้นก็ตัดสินใจให้เฟรมที่ยาวเกินไปเป็น error ด้วย ผู้เขียนใช้ uIP อยู่ แต่ผมสงสัยว่า Linux หรือ OS สมัยใหม่อื่น ๆ จัดการอย่างไร และอยากรู้ด้วยว่าเขาได้ทดสอบ interoperability หรือเปล่า
ถ้าแอปพลิเคชันไม่ได้ดูข้อมูล L2, Linux IP stack ก็จะเพิกเฉยไปเลย
เล็กกว่าชุดการ์ด SSI Ethernet รุ่นแรกของ DEC ในเชิงกายภาพมาก: https://i.ebayimg.com/images/g/NEYAAOSw-mZlg0lZ/s-l1600.jpg
บอร์ด DEC DEUNA ยาวเกิน 1 ฟุต แต่ก็มีฟังก์ชันเยอะกว่ามาก DEUNA เป็น NIC “ของจริง” มีคิวส่งและรับ แล้วจัดการมันได้เอง รวมถึงทำ DMA ด้วย แน่นอนว่าบนการ์ดยังมี PDP-11 ของตัวเองสำหรับรันงานนั้นด้วย
เจ๋งจริง ๆ อยากรู้ว่าใช้เวลานานแค่ไหน
แสดงให้เห็นว่าการฝัง backdoor ไว้ในชิปที่เชื่อมต่อกับพอร์ตเครือข่ายนั้นง่ายแค่ไหน
ในวิชา Communication Systems Engineering ผมเคย implement การประมวลผลสัญญาณ Ethernet แล้วต่อด้วย TCP/IP stack รวมถึง ARP และ switching ด้วย Motorola 68k QUIC assembly
เป็น 18 เดือนที่ยาวนานที่สุดในชีวิต