24 điểm bởi GN⁺ 13 ngày trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Phát triển trình điều khiển USB thường được xem là công việc ở cấp độ kernel, nhưng trên thực tế cũng có thể triển khai trong không gian người dùng với độ khó tương tự lập trình socket
  • Với libusb, bạn có thể thực hiện liệt kê thiết bị, truyền điều khiển và gửi/nhận dữ liệu mà không cần viết mã kernel
  • Giao tiếp USB gồm bốn kiểu truyền Control, Bulk, Interrupt, Isochronous cùng với hướng IN/OUT, và mỗi endpoint hoạt động như một kênh một chiều
  • Lấy giao thức Fastboot của thiết bị Android làm ví dụ, bài viết minh họa bằng mã cách gửi lệnh và nhận phản hồi qua endpoint Bulk
  • Ngay cả trong không gian người dùng, bạn vẫn có thể triển khai một trình điều khiển USB hoàn chỉnh, và mọi giao thức USB đều chia sẻ cùng một cấu trúc cơ bản

Giới thiệu

  • Trình điều khiển cho thiết bị USB thường khiến người ta cảm thấy khó vì nghĩ rằng phải xử lý mã kernel, nhưng thực tế chỉ có mức độ phức tạp tương đương ứng dụng dùng socket
  • Ngay cả các lập trình viên không có nhiều kinh nghiệm phần cứng cũng có thể học cách làm việc với USB trong không gian người dùng
  • Dù đã có tài liệu nói về chi tiết hoạt động của USB, người mới bắt đầu vẫn khó tiếp cận
  • Việc sử dụng USB không đòi hỏi kiến thức ở mức hệ thống nhúng, mà có thể tiếp cận giống như socket mạng

Thiết bị USB

  • Ví dụ sử dụng điện thoại thông minh Android ở chế độ bootloader
    • Dễ kiếm, giao thức đơn giản, và phù hợp để thử nghiệm vì hệ điều hành không có trình điều khiển mặc định
  • Cách vào chế độ bootloader khác nhau tùy thiết bị, nhưng thường có thể thực hiện bằng tổ hợp nút nguồn và nút âm lượng

Liệt kê thiết bị thủ công

  • Enumeration là quá trình host yêu cầu thông tin thiết bị để nhận diện nó, và được thực hiện tự động khi thiết bị được kết nối
  • Với thiết bị chuẩn, trình điều khiển được tự động nạp dựa trên USB class, còn thiết bị dành riêng cho nhà sản xuất dùng VID (Vendor ID) và PID (Product ID)
  • Trên Linux, có thể kiểm tra thông tin thiết bị bằng lệnh lsusb
    • Ví dụ: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
    • 18d1VID của Google, 4ee0PID của bootloader Nexus/Pixel
  • Có thể dùng lệnh lsusb -t để kiểm tra class và trạng thái driver
    • Nếu hiển thị Class=Vendor Specific Class, Driver=[none] thì hệ điều hành không nạp driver
  • Trên Windows, có thể xem cùng thông tin này bằng Device Manager hoặc USB Device Tree Viewer

Liệt kê thiết bị với libusb

  • Thư viện libusb cho phép giao tiếp với thiết bị USB trong không gian người dùng mà không cần viết mã kernel
  • Có thể dùng libusb_hotplug_register_callback() để thiết lập callback chạy khi thiết bị với cặp VID:PID cụ thể được kết nối
  • Khi chạy chương trình rồi cắm thiết bị vào, sẽ in ra thông báo "Device plugged in!"
  • Trên Linux, cách này hoạt động mặc định; nếu cần, có thể tách driver kernel bằng libusb_detach_kernel_driver()
  • Trên Windows, cần driver Winusb.sys; nếu chưa có, có thể thay thủ công bằng công cụ Zadig

Giao tiếp với thiết bị

  • Lần giao tiếp đầu tiên với thiết bị USB được thực hiện qua endpoint Control (địa chỉ 0x00)
  • Dùng libusb_control_transfer() để gửi yêu cầu chuẩn (GET_STATUS) và đọc trạng thái thiết bị
    • Ví dụ phản hồi: 01 00 → byte đầu là Self-Powered, byte thứ hai là không hỗ trợ Remote Wakeup
  • Sau đó có thể lấy descriptor của thiết bị bằng yêu cầu GET_DESCRIPTOR
    • Dữ liệu trả về bao gồm các thông tin như idVendor, idProduct, bDeviceClass
  • Có thể dùng lệnh lsusb -v để xem chi tiết mọi descriptor (thiết bị, cấu hình, interface, endpoint, v.v.)
    • Ví dụ: interface Android Fastboot có các endpoint Bulk IN (0x81)Bulk OUT (0x02)

Endpoint

  • Endpoint là khái niệm tương tự cổng mạng, là kênh để thiết bị gửi và nhận dữ liệu
  • Mỗi descriptor định nghĩa loại và hướng của từng endpoint
  • Kiểu truyền Control

    • Mọi thiết bị đều có một endpoint kiểu này và địa chỉ luôn là 0x00
    • Dùng cho thiết lập ban đầu và yêu cầu thông tin thiết bị
    • Không thuộc về interface nào mà tồn tại như một phần của chính thiết bị
  • Kiểu truyền Bulk

    • Dùng cho truyền dữ liệu lớn không thời gian thực
    • Ví dụ: Mass Storage, CDC-ACM (serial), RNDIS (Ethernet)
    • Băng thông cao nhưng ưu tiên thấp
  • Kiểu truyền Interrupt

    • Dùng cho truyền lượng dữ liệu nhỏ với độ trễ thấp
    • Thường dùng để polling nhanh thao tác nút bấm trên bàn phím, chuột, v.v.
    • Đây không phải ngắt phần cứng thực sự; host sẽ yêu cầu theo chu kỳ
  • Kiểu truyền Isochronous

    • Dùng cho dữ liệu lớn nhạy cảm về thời gian như truyền phát âm thanh, video
    • Nếu phát sinh độ trễ, chất lượng sẽ giảm rõ rệt ngay lập tức
    • Trong libusb, kiểu này được xử lý theo phương thức bất đồng bộ
  • Hướng IN / OUT

    • USB là kiến trúc lấy host làm trung tâm, nên thiết bị không thể tự gửi dữ liệu trước khi nhận yêu cầu
    • IN: hướng host nhận dữ liệu
    • OUT: hướng host gửi dữ liệu
    • Nếu bit cao nhất (MSB) của địa chỉ endpoint là 1 thì là IN, 0 thì là OUT
    • Có thể dùng tối đa 127 endpoint do người dùng định nghĩa (0x00 dành riêng cho Control)
    • Endpoint là một chiều, nên thường được ghép thành cặp IN/OUT như ở interface Fastboot

Giao thức Fastboot

  • Fastboot là giao thức giao tiếp với bootloader Android, hoạt động theo cấu trúc gửi chuỗi lệnh rồi nhận mã trạng thái 4 byte và dữ liệu
    • Ví dụ:
      • Host: "getvar:version"Client: "OKAY0.4"
      • Host: "getvar:nonexistant"Client: "OKAY"
  • Ví dụ mã gửi lệnh Fastboot bằng libusb
    • Chiếm quyền interface 0 bằng libusb_claim_interface()
    • Gửi lệnh "getvar:version" tới endpoint Bulk OUT (0x02)
    • Nhận phản hồi từ endpoint Bulk IN (0x81)
    • Ví dụ đầu ra:
      Request: getvar:version
      Response: OKAY0.4
      
    • OKAY là trạng thái thành công, 0.4 là phiên bản Fastboot

Kết luận

  • Có thể triển khai một trình điều khiển USB hoàn chỉnh trong không gian người dùng mà không cần viết mã kernel
  • Mọi trình điều khiển USB đều tuân theo cùng một nguyên lý cơ bản, chỉ khác nhau ở giao thức
  • Ngay cả các giao thức phức tạp như MTP cũng có cùng cấu trúc nền tảng và có thể tiếp cận bằng khái niệm tương tự giao tiếp socket

1 bình luận

 
Ý kiến trên Hacker News
  • Đúng là thời điểm hoàn hảo. Tôi sắp nhận một chiếc MOTU MIDI Express XT từ cửa hàng Guitar Center gần nhà
    Đây là đồ cũ nên phải chờ vì theo luật họ phải giữ lại trong một khoảng thời gian nhất định. Vấn đề là thiết bị này không dùng MIDI-over-USB tiêu chuẩn mà dùng giao thức độc quyền, nên trên các hệ thống của tôi như Linux, OpenBSD hay Haiku thì không thể dùng trực tiếp qua USB
    Trước mắt tôi chỉ cần định tuyến giữa mô-đun synth và controller nên vẫn ổn, nhưng sẽ rất tốt nếu làm cho nó hoạt động cả phía PC
    Có một driver Linux hiện có, nhưng độ ổn định còn chưa rõ và cũng không chắc có hỗ trợ XT hay không. Nghe nói vấn đề kernel panic đã được xử lý nhưng vẫn còn issue
    Vì vậy tôi định tự viết một driver không gian người dùng dựa trên LibUSB. Nếu có thể expose các cổng MIDI và thêm công cụ định tuyến thì sẽ khá hữu ích

    • Thời gian chờ ở Guitar Center không chỉ đơn thuần là để kiểm tra xem có phải đồ ăn cắp hay không. Theo luật họ còn phải tuân thủ thời gian cấm bán nhất định như tiệm cầm đồ (pawn shop), để chủ sở hữu gốc có thời gian chuộc lại trước khi được phép bán
    • Tôi cũng dùng đúng thiết bị đó và đã đóng gói driver ấy lên AUR. Binary blob không hoạt động, nhưng để dùng như bộ định tuyến MIDI đơn giản thì đủ tốt
  • Nếu muốn thử làm kiểu này bằng Go, tôi đã tạo thư viện go-usb cho phép truy cập USB mà không cần cgo
    Tôi cũng đã phát triển go-uvc để xử lý thiết bị UVC bằng nó

    • Với Rust thì tôi khuyên dùng nusb
  • Gần đây tôi cũng đang triển khai hệ thống usbip trên Macbook M3 theo cách khá tương tự
    Tuy nhiên macOS mới có một số hạn chế. Với các thiết bị USB mà hệ thống đã nhận diện, bạn không thể xây dựng driver không gian người dùng dựa trên libusb trừ khi tự tắt các tính năng bảo mật

    • Phần override driver chỉ cần điều chỉnh ở một lớp nên vẫn có thể giảm nhẹ vấn đề này
  • Cách tiếp cận này rốt cuộc khiến driver USB cũng đóng vai trò như mã ứng dụng. Tức là nó gần với thư viện + chương trình hơn là driver
    Ví dụ, tôi tò mò không biết sẽ phải làm thế nào nếu muốn nối một thiết bị USB-Ethernet thành adapter mạng của OS

    • Các thiết bị được chuẩn hóa thường dùng USB/CDC/ECM hoặc RNDIS nên sẽ được nhận diện tự động. Truy cập từ không gian người dùng lại hữu ích hơn cho thiết bị không chuẩn. Trên Windows, có thể triển khai kiểu portable bằng libusb mà không cần ký driver
    • Trên Linux, bạn có thể tạo thiết bị tun/tap để giao tiếp với kernel từ không gian người dùng, hoặc buộc phải chạy các subsystem khác cũng trong không gian người dùng
  • Giá như tôi đọc được bài này vài năm trước thì lúc reverse engineer các chức năng của laptop đã dễ hơn rất nhiều. Đặc biệt, chương trình điều khiển LED bàn phím vẫn là một trong những dự án tôi thích nhất đến giờ

  • Đây thật sự là một bài nhập môn rất hữu ích. Làm việc với API phần cứng mức thấp vừa khó vừa đáng giá. Nhờ các lớp trừu tượng của OS hiện đại mà mọi thứ đã dễ hơn, nhưng hiểu được phần bên dưới vẫn rất quan trọng

  • Đoạn mã C++ trông hơi kỳ. Tôi chưa từng thấy bàn phím nào có thể gõ trực tiếp ký tự mũi tên

    • Đó là ligature của font lập trình. Nếu copy thì thực tế nó sẽ hiện là ->. Đây là cú pháp trailing return type của C++ hiện đại
    • Một số lập trình viên thích font có ligature. Nó gộp hai ký tự thành một glyph
    • Nếu cấu hình phím Compose thì với bàn phím nào bạn cũng có thể nhập “→”
    • Rốt cuộc thì nó chỉ là "->". Font chỉ render nó thành mũi tên thôi
  • Tôi từng thắc mắc liệu thiết bị USB có hỗ trợ DMA không. Nó chỉ có thể thực hiện thông qua host hay thiết bị cũng có thể truy cập bộ nhớ trực tiếp?

    • Thiết bị USB không truy cập trực tiếp bộ nhớ host như PCIe hay FireWire. Thay vào đó, bộ điều khiển XHCI thực hiện DMA, và hầu hết bộ điều khiển thiết bị đều hỗ trợ DMA giữa RAM riêng của chúng với USB
    • Mọi truyền dữ liệu đều do host chủ động. Dù có vẻ như thiết bị tự gửi dữ liệu trước, thực tế vẫn là host yêu cầu. DMA trực tiếp sẽ là một rủi ro bảo mật lớn
  • Trước đây tôi từng muốn làm một thiết bị USB đơn giản, nhưng gần như không có thông tin về cách viết descriptor. Phần lớn chỉ là kiểu “hãy tìm một thiết bị tương tự rồi copy và sửa thử”. Điều đó khiến tôi tự hỏi USB có thực sự là một chuẩn tốt hay không

    • Tôi cũng từng thấy descriptor rất bí hiểm, nhưng rồi nhận ra nó chỉ là struct nhị phân cố định. Chỉ cần khớp đúng các field và endpoint mà từng lớp USB quy định là sẽ được nhận diện
    • USB thì ổn, nhưng xét về điện thì USB 1/2 không phải tín hiệu vi sai thực sự
    • Tài liệu tutorial thì gần như không có, nhưng với một chuẩn do các tập đoàn lớn xây dựng thì nó khá hợp lý. Chỉ là vì có quá nhiều lựa chọn nên phải đọc rất nhiều spec liên quan
  • Nếu ai đó yêu cầu tôi “tự viết driver thiết bị USB”, tôi sẽ trả lại thiết bị đó và trước tiên kiểm tra xem có thể xử lý nó như cổng COM ảo hay không