- การพัฒนาไดรเวอร์ 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
- จองอินเทอร์เฟซ 0 ด้วย
สรุป
- สามารถ สร้างไดรเวอร์ USB ที่สมบูรณ์ใน user space ได้โดยไม่ต้องเขียนโค้ดเคอร์เนล
- ไดรเวอร์ USB ทั้งหมดทำงานบนหลักการพื้นฐานเดียวกัน ต่างกันเพียงโปรโตคอล
- แม้โปรโตคอลที่ซับซ้อนกว่าอย่าง MTP ก็ยังมีโครงสร้างพื้นฐานเหมือนกัน และสามารถมองได้ด้วยแนวคิดที่ คล้ายกับการสื่อสารผ่าน socket
1 ความคิดเห็น
ความคิดเห็นจาก 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 ก็น่าจะมีประโยชน์มาก
ถ้าอยากลองทำอะไรแบบนี้ด้วย Go ฉันมีไลบรารี go-usb ที่เข้าถึง USB ได้โดยไม่ต้องใช้ cgo
ฉันยังใช้มันพัฒนา go-uvc สำหรับจัดการ อุปกรณ์ UVC ด้วย
เมื่อไม่นานมานี้ฉันก็กำลังทำระบบ usbip บน Macbook M3 ด้วยแนวทางคล้ายกัน
แต่ macOS รุ่นใหม่มีข้อจำกัดอยู่ สำหรับอุปกรณ์ USB ที่ระบบรู้จักแล้ว จะสร้าง ไดรเวอร์ใน userspace บน libusb ไม่ได้ เว้นแต่จะปิดฟีเจอร์ความปลอดภัยด้วยตนเอง
แนวทางนี้สุดท้ายแล้วทำให้ไดรเวอร์ USB กลายเป็นโค้ดแอปพลิเคชันไปด้วย พูดอีกแบบคือมัน ใกล้เคียงไลบรารี+โปรแกรมมากกว่าจะเป็นไดรเวอร์
อย่างเช่น ถ้าอยากให้ USB-Ethernet device เชื่อมเป็น network adapter ของระบบปฏิบัติการ จะต้องทำอย่างไรบ้างก็น่าสงสัย
ถ้าฉันได้อ่านบทความนี้ตั้งแต่ หลายปีก่อน ตอนที่กำลัง reverse engineer ฟังก์ชันของโน้ตบุ๊ก มันคงง่ายกว่านี้มาก โดยเฉพาะ โปรแกรมควบคุมไฟ LED ของคีย์บอร์ด ที่จนถึงตอนนี้ก็ยังเป็นหนึ่งในโปรเจกต์ที่ฉันชอบที่สุด
เป็นบทนำที่มีประโยชน์มาก การทำงานกับ API ฮาร์ดแวร์ระดับล่าง นั้นยากแต่คุ้มค่า แม้ abstraction layer ของระบบปฏิบัติการสมัยใหม่จะช่วยให้ทุกอย่างง่ายขึ้น แต่การเข้าใจสิ่งที่อยู่ข้างใต้ก็ยังสำคัญอยู่ดี
โค้ด C++ ดูแปลกมาก ฉันไม่เคยเห็นคีย์บอร์ดที่พิมพ์ลูกศรได้โดยตรงเลย
->มันคือไวยากรณ์ trailing return type ของ C++ สมัยใหม่"->"นั่นแหละ ฟอนต์แค่เรนเดอร์ให้เป็นลูกศรสงสัยว่าอุปกรณ์ USB รองรับ DMA หรือไม่ มันทำได้เฉพาะผ่านโฮสต์เท่านั้นหรืออุปกรณ์เข้าถึงหน่วยความจำได้โดยตรงด้วย
เมื่อก่อนฉันเคยพยายามทำอุปกรณ์ USB แบบง่าย ๆ แต่แทบไม่มีข้อมูลเรื่องวิธีเขียน descriptor เลย ส่วนใหญ่มีแต่คำแนะนำแนวว่า “หาอุปกรณ์คล้าย ๆ กันแล้วคัดลอกมาปรับดู” เลยอดสงสัยไม่ได้ว่า USB เป็นมาตรฐานที่ดีจริงหรือเปล่า
ถ้ามีคนมาขอให้ “เขียนไดรเวอร์อุปกรณ์ USB เอง” ฉันคงคืนอุปกรณ์ชิ้นนั้นไปก่อน แล้วเช็กก่อนว่าใช้ virtual COM port แทนได้ไหม