24 คะแนน โดย GN⁺ 21 일 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การพัฒนาไดรเวอร์ USB มักถูกมองว่าเป็นงานระดับเคอร์เนล แต่จริง ๆ แล้วสามารถทำใน user space ได้ด้วยความยากใกล้เคียงกับ socket programming
  • เมื่อใช้ libusb ก็สามารถทำทั้งการ enumerate อุปกรณ์, control transfer และการรับส่งข้อมูลได้โดยไม่ต้องเขียนโค้ดเคอร์เนล
  • การสื่อสารผ่าน USB ประกอบด้วยการส่งข้อมูล 4 แบบคือ Control, Bulk, Interrupt, Isochronous และมีทิศทาง IN/OUT โดยแต่ละ endpoint ทำงานเป็นช่องทางแบบทิศทางเดียว
  • ยกตัวอย่างด้วย โปรโตคอล Fastboot ของอุปกรณ์ Android พร้อมสาธิตโค้ดสำหรับส่งคำสั่งและรับคำตอบผ่าน Bulk endpoint
  • แม้ใน user space ก็สามารถ สร้างไดรเวอร์ USB ที่สมบูรณ์ได้ และโปรโตคอล USB ทั้งหมดมีโครงสร้างพื้นฐานแบบเดียวกัน

แนะนำ

  • ไดรเวอร์สำหรับอุปกรณ์ USB มักดูยากเพราะหลายคนคิดว่าต้องแตะโค้ดเคอร์เนล แต่ในความเป็นจริง มีความซับซ้อนระดับเดียวกับแอปที่ใช้ socket
  • แม้นักพัฒนาจะไม่ได้มีประสบการณ์ด้านฮาร์ดแวร์มาก ก็สามารถเรียนรู้ วิธีจัดการ USB ใน user space ได้
  • แม้จะมีเอกสารที่อธิบายรายละเอียดการทำงานของ USB อยู่ แต่สำหรับผู้เริ่มต้นมักเข้าถึงได้ยาก
  • การใช้งาน USB ไม่ได้ต้องการความรู้ระดับระบบ embedded และสามารถเข้าถึงได้เหมือนการทำงานกับ network socket

อุปกรณ์ USB

  • ใช้ สมาร์ตโฟน Android ในโหมด bootloader เป็นตัวอย่าง
    • หาได้ง่าย โปรโตคอลไม่ซับซ้อน และเหมาะกับการทดลองเพราะระบบปฏิบัติการไม่มีไดรเวอร์พื้นฐานมาให้
  • วิธีเข้าสู่โหมด bootloader แตกต่างกันไปตามอุปกรณ์ แต่โดยทั่วไปมักทำได้ด้วยการกดปุ่มเปิดเครื่องร่วมกับปุ่มเพิ่ม/ลดเสียง

การ enumerate อุปกรณ์แบบแมนนวล

  • Enumeration คือกระบวนการที่โฮสต์ร้องขอข้อมูลของอุปกรณ์เพื่อระบุตัวตนของมัน และจะทำโดยอัตโนมัติเมื่อมีการเชื่อมต่ออุปกรณ์
  • อุปกรณ์มาตรฐานจะโหลดไดรเวอร์อัตโนมัติตาม USB class ส่วน vendor-specific device จะใช้ VID (Vendor ID) และ PID (Product ID)
  • บน Linux สามารถตรวจสอบข้อมูลอุปกรณ์ได้ด้วยคำสั่ง lsusb
    • ตัวอย่าง: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
    • 18d1 คือ VID ของ Google และ 4ee0 คือ PID ของ bootloader บน Nexus/Pixel
  • ใช้คำสั่ง lsusb -t เพื่อตรวจสอบ class และสถานะไดรเวอร์ ได้
    • หากแสดง Class=Vendor Specific Class, Driver=[none] หมายความว่า OS ยังไม่ได้โหลดไดรเวอร์
  • บน Windows สามารถดูข้อมูลเดียวกันได้จาก Device Manager หรือ USB Device Tree Viewer

การ enumerate อุปกรณ์ด้วย libusb

  • ไลบรารี libusb ช่วยให้สื่อสารกับอุปกรณ์ USB จาก user space ได้โดยไม่ต้องเขียนโค้ดเคอร์เนล
  • สามารถตั้งค่า libusb_hotplug_register_callback() ให้เรียก callback เมื่อมีการเชื่อมต่ออุปกรณ์ที่ตรงกับคู่ VID:PID ที่กำหนด
  • เมื่อรันโปรแกรมแล้วเสียบอุปกรณ์ จะมีข้อความ "Device plugged in!" แสดงขึ้น
  • บน Linux มักใช้งานได้ทันที และหากจำเป็นสามารถแยกเคอร์เนลไดรเวอร์ออกได้ด้วย libusb_detach_kernel_driver()
  • บน Windows ต้องใช้ไดรเวอร์ Winusb.sys และหากไม่มีสามารถใช้เครื่องมือ Zadig เพื่อเปลี่ยนไดรเวอร์ด้วยตนเองได้

การสื่อสารกับอุปกรณ์

  • การสื่อสารครั้งแรกกับอุปกรณ์ USB จะทำผ่าน Control endpoint (ที่อยู่ 0x00)
  • ใช้ libusb_control_transfer() เพื่อส่ง standard request (GET_STATUS) และอ่านสถานะของอุปกรณ์
    • ตัวอย่างคำตอบ: 01 00 → ไบต์แรกหมายถึง Self-Powered ส่วนไบต์ที่สองหมายถึง ไม่รองรับ Remote Wakeup
  • หลังจากนั้นสามารถใช้คำขอ GET_DESCRIPTOR เพื่อดึง descriptor ของอุปกรณ์ได้
    • ข้อมูลที่ได้จะมีรายละเอียดอย่าง idVendor, idProduct, bDeviceClass เป็นต้น
  • ใช้คำสั่ง lsusb -v เพื่อตรวจสอบ descriptor ทั้งหมดอย่างละเอียดได้ (อุปกรณ์, การกำหนดค่า, อินเทอร์เฟซ, endpoint ฯลฯ)
    • ตัวอย่าง: อินเทอร์เฟซ Android Fastboot มี endpoint แบบ Bulk IN(0x81) และ Bulk OUT(0x02)

Endpoint

  • Endpoint เป็น แนวคิดที่คล้ายกับ network port ซึ่งเป็นช่องทางที่อุปกรณ์ใช้รับส่งข้อมูล
  • ใน descriptor จะกำหนดชนิดและทิศทางของแต่ละ endpoint ไว้
  • การส่งข้อมูลแบบ Control

    • มีอยู่หนึ่งตัวในทุกอุปกรณ์ และมีที่อยู่เป็น 0x00 เสมอ
    • ใช้สำหรับการตั้งค่าเริ่มต้นและร้องขอข้อมูลของอุปกรณ์
    • ไม่ได้อยู่ภายใต้อินเทอร์เฟซ แต่เป็นส่วนหนึ่งของตัวอุปกรณ์เอง
  • การส่งข้อมูลแบบ Bulk

    • ใช้สำหรับ การส่งข้อมูลปริมาณมากที่ไม่ต้องการความเป็น real-time
    • ตัวอย่าง: Mass Storage, CDC-ACM (serial), RNDIS (Ethernet)
    • แบนด์วิดท์สูง แต่ลำดับความสำคัญต่ำ
  • การส่งข้อมูลแบบ Interrupt

    • ใช้สำหรับ ข้อมูลขนาดเล็กที่ต้องการความหน่วงต่ำ
    • เช่น คีย์บอร์ด เมาส์ ที่ต้อง polling การกดปุ่มอย่างรวดเร็ว
    • ไม่ใช่ hardware interrupt จริง ๆ แต่เป็นการที่โฮสต์ร้องขอเป็นระยะ
  • การส่งข้อมูลแบบ Isochronous

    • ใช้สำหรับ ข้อมูลปริมาณมากที่ไวต่อเวลา เช่น audio และ video streaming
    • หากเกิดความล่าช้า คุณภาพจะลดลงทันที
    • ใน libusb จะจัดการแบบ asynchronous
  • ทิศทาง IN / OUT

    • USB เป็น โครงสร้างแบบ host-centric โดยอุปกรณ์จะไม่ส่งข้อมูลออกมาก่อนจนกว่าจะได้รับคำขอ
    • IN: ทิศทางที่โฮสต์เป็นฝ่ายรับข้อมูล
    • OUT: ทิศทางที่โฮสต์เป็นฝ่ายส่งข้อมูล
    • หากบิตบนสุด (MSB) ของที่อยู่ endpoint เป็น 1 จะเป็น IN และถ้าเป็น 0 จะเป็น OUT
    • สามารถใช้ endpoint ที่ผู้ใช้กำหนดเองได้สูงสุด 127 จุด (0x00 ใช้สำหรับ Control เท่านั้น)
    • Endpoint เป็น แบบทิศทางเดียว และมักจับคู่เป็น IN/OUT เหมือนในอินเทอร์เฟซ Fastboot

โปรโตคอล Fastboot

  • Fastboot คือโปรโตคอลสื่อสารกับ bootloader ของ Android โดยมีโครงสร้างเป็นการส่งสตริงคำสั่ง แล้วรับรหัสสถานะ 4 ไบต์พร้อมข้อมูลกลับมา
    • ตัวอย่าง:
      • Host: "getvar:version"Client: "OKAY0.4"
      • Host: "getvar:nonexistant"Client: "OKAY"
  • ตัวอย่างโค้ดสำหรับส่งคำสั่ง Fastboot ด้วย libusb
    • จองอินเทอร์เฟซ 0 ด้วย libusb_claim_interface()
    • ส่งคำสั่ง "getvar:version" ไปยัง endpoint Bulk OUT(0x02)
    • รับคำตอบจาก endpoint Bulk IN(0x81)
    • ตัวอย่างผลลัพธ์:
      Request: getvar:version
      Response: OKAY0.4
      
    • OKAY คือสถานะสำเร็จ และ 0.4 คือเวอร์ชันของ Fastboot

สรุป

  • สามารถ สร้างไดรเวอร์ USB ที่สมบูรณ์ใน user space ได้โดยไม่ต้องเขียนโค้ดเคอร์เนล
  • ไดรเวอร์ USB ทั้งหมดทำงานบนหลักการพื้นฐานเดียวกัน ต่างกันเพียงโปรโตคอล
  • แม้โปรโตคอลที่ซับซ้อนกว่าอย่าง MTP ก็ยังมีโครงสร้างพื้นฐานเหมือนกัน และสามารถมองได้ด้วยแนวคิดที่ คล้ายกับการสื่อสารผ่าน socket

1 ความคิดเห็น

 
GN⁺ 21 일 전
ความคิดเห็นจาก Hacker News
  • จังหวะมาพอดีเป๊ะเลย อีกไม่นานฉันจะไปรับ MOTU MIDI Express XT ที่ Guitar Center แถวบ้าน
    เป็นอุปกรณ์มือสองเลยต้องรอตามกฎหมายที่บังคับให้พักของไว้ช่วงหนึ่งอยู่ ปัญหาคืออุปกรณ์นี้ไม่ได้ใช้ MIDI-over-USB มาตรฐาน แต่ใช้โปรโตคอลเฉพาะ ทำให้ใช้ผ่าน USB ตรง ๆ บนระบบของฉันอย่าง Linux, OpenBSD หรือ Haiku ไม่ได้
    ตอนนี้แค่ต้องการทำ routing ระหว่างซินธ์โมดูลกับคอนโทรลเลอร์ก็ยังพอไหว แต่ถ้าทำให้ฝั่ง PC ใช้งานได้ด้วยก็คงดี
    มี ไดรเวอร์ Linux ที่มีอยู่แล้ว แต่ก็ยังไม่แน่ใจเรื่องความเสถียร และไม่ชัดว่า XT รองรับหรือเปล่า บอกว่าแก้ปัญหา kernel panic แล้ว แต่ก็ยังมี issue ค้างอยู่
    เลยคิดจะเขียน ไดรเวอร์ใน userspace บน LibUSB เอง ถ้าทำให้มองเห็น MIDI port ได้และเพิ่ม tooling สำหรับ routing ก็น่าจะมีประโยชน์มาก

    • ช่วงเวลารอของ Guitar Center ไม่ได้มีไว้แค่ตรวจว่าเป็นของขโมยมาหรือไม่เท่านั้น กฎหมายบังคับให้ต้องห้ามขายช่วงหนึ่งเหมือน โรงรับจำนำ (pawn shop) เพื่อให้เจ้าของเดิมมีเวลามาไถ่คืนก่อนถึงจะขายได้
    • ฉันก็ใช้อุปกรณ์ตัวเดียวกันและเคยแพ็กเกจไดรเวอร์นั้นลง AUR แล้ว binary blob ใช้งานไม่ได้ แต่ถ้าใช้เป็น MIDI router แบบง่าย ๆ ก็เพียงพอ
  • ถ้าอยากลองทำอะไรแบบนี้ด้วย Go ฉันมีไลบรารี go-usb ที่เข้าถึง USB ได้โดยไม่ต้องใช้ cgo
    ฉันยังใช้มันพัฒนา go-uvc สำหรับจัดการ อุปกรณ์ UVC ด้วย

    • ถ้าเป็น Rust แนะนำ nusb
  • เมื่อไม่นานมานี้ฉันก็กำลังทำระบบ usbip บน Macbook M3 ด้วยแนวทางคล้ายกัน
    แต่ macOS รุ่นใหม่มีข้อจำกัดอยู่ สำหรับอุปกรณ์ USB ที่ระบบรู้จักแล้ว จะสร้าง ไดรเวอร์ใน userspace บน libusb ไม่ได้ เว้นแต่จะปิดฟีเจอร์ความปลอดภัยด้วยตนเอง

    • การ override ไดรเวอร์ต้องปรับแค่ระดับชั้นเดียว เลยพอบรรเทาได้
  • แนวทางนี้สุดท้ายแล้วทำให้ไดรเวอร์ USB กลายเป็นโค้ดแอปพลิเคชันไปด้วย พูดอีกแบบคือมัน ใกล้เคียงไลบรารี+โปรแกรมมากกว่าจะเป็นไดรเวอร์
    อย่างเช่น ถ้าอยากให้ USB-Ethernet device เชื่อมเป็น network adapter ของระบบปฏิบัติการ จะต้องทำอย่างไรบ้างก็น่าสงสัย

    • อุปกรณ์มาตรฐานมักใช้ USB/CDC/ECM หรือ RNDIS อยู่แล้ว เลยถูกตรวจจับอัตโนมัติ การเข้าถึงจาก userspace กลับมีประโยชน์กับ อุปกรณ์ที่ไม่เป็นมาตรฐาน มากกว่า บน Windows ก็ทำแบบพกพาได้ด้วย libusb โดยไม่ต้องมีการเซ็นไดรเวอร์
    • บน Linux คุณสามารถสร้าง อุปกรณ์ tun/tap เพื่อสื่อสารระหว่าง userspace กับเคอร์เนลได้ หรือไม่ก็ต้องรัน subsystem อื่นใน userspace ด้วย
  • ถ้าฉันได้อ่านบทความนี้ตั้งแต่ หลายปีก่อน ตอนที่กำลัง reverse engineer ฟังก์ชันของโน้ตบุ๊ก มันคงง่ายกว่านี้มาก โดยเฉพาะ โปรแกรมควบคุมไฟ LED ของคีย์บอร์ด ที่จนถึงตอนนี้ก็ยังเป็นหนึ่งในโปรเจกต์ที่ฉันชอบที่สุด

  • เป็นบทนำที่มีประโยชน์มาก การทำงานกับ API ฮาร์ดแวร์ระดับล่าง นั้นยากแต่คุ้มค่า แม้ abstraction layer ของระบบปฏิบัติการสมัยใหม่จะช่วยให้ทุกอย่างง่ายขึ้น แต่การเข้าใจสิ่งที่อยู่ข้างใต้ก็ยังสำคัญอยู่ดี

  • โค้ด C++ ดูแปลกมาก ฉันไม่เคยเห็นคีย์บอร์ดที่พิมพ์ลูกศรได้โดยตรงเลย

    • นั่นคือ ligature ของฟอนต์สำหรับเขียนโปรแกรม ถ้าคัดลอกออกมาจริง ๆ จะเห็นเป็น -> มันคือไวยากรณ์ trailing return type ของ C++ สมัยใหม่
    • นักพัฒนาบางคนชอบฟอนต์แบบ ligature เพราะมันรวมอักขระสองตัวให้เป็น glyph เดียว
    • ถ้าตั้งค่า Compose key คุณก็พิมพ์ “→” ได้จากคีย์บอร์ดไหนก็ได้
    • สรุปแล้วมันก็แค่ "->" นั่นแหละ ฟอนต์แค่เรนเดอร์ให้เป็นลูกศร
  • สงสัยว่าอุปกรณ์ USB รองรับ DMA หรือไม่ มันทำได้เฉพาะผ่านโฮสต์เท่านั้นหรืออุปกรณ์เข้าถึงหน่วยความจำได้โดยตรงด้วย

    • อุปกรณ์ USB ไม่ได้เข้าถึงหน่วยความจำของโฮสต์โดยตรงเหมือน PCIe หรือ FireWire แต่ คอนโทรลเลอร์ XHCI จะเป็นตัวทำ DMA และคอนโทรลเลอร์ของอุปกรณ์ส่วนใหญ่ก็รองรับ DMA ระหว่าง RAM ภายในกับ USB เอง
    • การส่งข้อมูลทุกอย่างนั้น โฮสต์เป็นฝ่ายเริ่มต้น แม้จะดูเหมือนว่าอุปกรณ์ส่งข้อมูลมาก่อน แต่จริง ๆ คือโฮสต์เป็นฝ่ายร้องขอ การมี DMA โดยตรงจะเป็นความเสี่ยงด้านความปลอดภัยอย่างมาก
  • เมื่อก่อนฉันเคยพยายามทำอุปกรณ์ USB แบบง่าย ๆ แต่แทบไม่มีข้อมูลเรื่องวิธีเขียน descriptor เลย ส่วนใหญ่มีแต่คำแนะนำแนวว่า “หาอุปกรณ์คล้าย ๆ กันแล้วคัดลอกมาปรับดู” เลยอดสงสัยไม่ได้ว่า USB เป็นมาตรฐานที่ดีจริงหรือเปล่า

    • ฉันเองก็เคยรู้สึกว่า descriptor เป็นของลึกลับอยู่พักหนึ่ง ก่อนจะเข้าใจว่าจริง ๆ แล้วมันคือ struct ไบนารีแบบตายตัว แค่ใส่ field และ endpoint ให้ตรงตามที่ USB class นั้นกำหนด ระบบก็จะรู้จัก
    • USB ก็โอเคนะ แต่ในเชิงไฟฟ้า USB 1/2 ไม่ใช่สัญญาณแบบ differential ที่แท้จริง
    • แทบไม่มีสื่อสอนแบบ tutorial เลย แต่ถ้าเทียบกับมาตรฐานจากบริษัทใหญ่ก็ถือว่าสมเหตุสมผลพอสมควร เพียงแต่มี ตัวเลือกเยอะเกินไป เลยต้องอ่านสเปกที่เกี่ยวข้องเยอะมาก
  • ถ้ามีคนมาขอให้ “เขียนไดรเวอร์อุปกรณ์ USB เอง” ฉันคงคืนอุปกรณ์ชิ้นนั้นไปก่อน แล้วเช็กก่อนว่าใช้ virtual COM port แทนได้ไหม