2010-07-30 201 views
19

我正在嘗試與相當特定的USB設備進行通信並開發Windows和Mac代碼。在Mac上讀取和寫入USB(HID)中斷端點

該設備是一個帶有HID接口(3類)的USB設備,具有兩個端點,一箇中斷輸入和一箇中斷輸出。設備的本質是隻有當數據從主機請求時才從輸入端點上的設備發出數據:主機向其發送其設備在其輸入中斷端點上響應的數據。將數據寫入設備(寫入)要簡單得多...

Windows的代碼非常簡單:我得到設備句柄,然後調用ReadFile或WriteFile。顯然,很多基礎異步行爲都被抽象出來了。它似乎工作正常。

但是,在Mac上,它有點粘。我已經嘗試了很多東西,但沒有一個已經完全成功,但這裏有兩件事似乎最有前途......

1.)試圖通過IOUSBInterfaceInterface訪問設備(如USB),迭代通過端點確定輸入和輸出端點,並(希望)使用ReadPipe和WritePipe進行通信。不幸的是,我一旦擁有它就無法打開接口,返回值(kIOReturnExclusiveAccess)注意到某些東西已經使設備獨佔打開。我嘗試過使用IOUSBinterfaceInterface183,以便我可以調用USBInterfaceOpenSeize,但這會導致相同的返回錯誤值。

---更新2010年7月30日---
顯然,蘋果IOUSBHIDDriver年初到設備相匹配,這是什麼可能阻止打開IOUSBInterfaceInterface。從一些挖掘看來,防止IOUSBHIDDriver匹配的常見方式是編寫一個代碼更小的kext(內核擴展),並使用更高的探測分數。這可以儘早匹配,防止IOUSBHIDDriver打開設備,並且理論上應該允許我打開接口並直接寫入和讀取端點。這是好的,但我更願意不必在用戶機器上安裝額外的東西。如果有人知道一個可靠的選擇,我會感謝信息。

2.)打開設備作爲IOHIDDeviceInterface122(或更高版本)。爲了讀取,我設置了一個異步端口,事件源和回調方法,當數據準備就緒時 - 當數據從輸入中斷端點上的設備發送時。但是,要寫入設備需要的數據來初始化響應,我找不到方法。我很難過。 setReport通常寫入控制端點,另外我需要寫一個不會期望任何直接響應,也不會阻塞的寫入。

我在網上看了一遍,嘗試了很多東西,但沒有一個能讓我成功。有什麼建議?我不能使用很多Apple HIDManager代碼,因爲大部分代碼都是10.5+,我的應用程序也必須在10.4上運行。

回答

28

我現在有一個可用的Mac驅動程序到需要通過中斷端點進行通信的USB設備。以下是我的做法:

最終,對我而言效果最好的方法是選項1(如上所述)。如上所述,我在向設備打開COM樣式的IOUSBInterfaceInterface時遇到了問題。隨着時間的推移,很明顯這是由於HIDManager捕獲設備造成的。一旦捕獲到設備(即使USBInterfaceOpenSeize調用或USBDeviceOpenSeize調用不起作用),我也無法從HIDManager控制設備。

要控制設備,我需要在HIDManager之前抓住它。解決方案是編寫無代碼kext(內核擴展)。kext基本上是一個包含在System/Library/Extensions中的包,通常包含plist(屬性列表)和(有時)內核級驅動程序等。在我的情況下,我只想要plist,它會向內核提供與它匹配的設備的說明。如果數據比HIDManager提供更高的探測分數,那麼我基本上可以捕獲設備並使用用戶空間驅動程序與其通信。

的內核擴展的plist寫着字,用修改了一些項目的具體細節,如下:

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 
<plist version="1.0"> 
<dict> 
    <key>OSBundleLibraries</key> 
    <dict> 
     <key>com.apple.iokit.IOUSBFamily</key> 
     <string>1.8</string> 
     <key>com.apple.kernel.libkern</key> 
     <string>6.0</string> 
    </dict> 
    <key>CFBundleDevelopmentRegion</key> 
    <string>English</string> 
    <key>CFBundleGetInfoString</key> 
    <string>Demi USB Device</string> 
    <key>CFBundleIdentifier</key> 
    <string>com.demiart.mydevice</string> 
    <key>CFBundleInfoDictionaryVersion</key> 
    <string>6.0</string> 
    <key>CFBundleName</key> 
    <string>Demi USB Device</string> 
    <key>CFBundlePackageType</key> 
    <string>KEXT</string> 
    <key>CFBundleSignature</key> 
    <string>????</string> 
    <key>CFBundleVersion</key> 
    <string>1.0.0</string> 
    <key>IOKitPersonalities</key> 
    <dict> 
     <key>Device Driver</key> 
     <dict> 
      <key>CFBundleIdentifier</key> 
      <string>com.apple.kernel.iokit</string> 
      <key>IOClass</key> 
      <string>IOService</string> 
      <key>IOProviderClass</key> 
      <string>IOUSBInterface</string> 
      <key>idProduct</key> 
      <integer>12345</integer> 
      <key>idVendor</key> 
      <integer>67890</integer> 
      <key>bConfigurationValue</key> 
      <integer>1</integer> 
      <key>bInterfaceNumber</key> 
      <integer>0</integer> 
     </dict> 
    </dict> 
    <key>OSBundleRequired</key> 
    <string>Local-Root</string> 
</dict> 
</plist> 

的idVendor和idProduct值給出的kext特異性和充分提高其探測積分。

爲了使用KEXT,下面的事情需要做(這我的安裝程序會爲客戶做):

  1. 更改所有者根:車輪(sudo chown root:wheel DemiUSBDevice.kext
  2. 複製KEXT以擴展(sudo cp DemiUSBDevice.kext /System/Library/Extensions
  3. 調用kextload實用程序加載立即使用的kext不重新啓動(sudo kextload -vt /System/Library/Extensions/DemiUSBDevice.kext
  4. 觸摸擴展文件夾,以便下一次重啓將強制緩存重建(sudo touch /System/Library/Extensions

此時系統應該使用kext保持HIDManager無法捕獲我的設備。現在,該怎麼辦?如何寫入和讀取它?

以下是我的代碼的簡化片段,減去任何錯誤處理,說明解決方案。在能夠對設備進行任何操作之前,應用程序需要知道設備何時附着(和分離)。請注意,這僅僅是爲了說明目的 - 一些變量是類級別的,有些是全球性的,等這裏是初始化代碼,設置安裝/拆卸活動起來:

#include <IOKit/IOKitLib.h> 
#include <IOKit/IOCFPlugIn.h> 
#include <IOKit/usb/IOUSBLib.h> 
#include <mach/mach.h> 

#define DEMI_VENDOR_ID 12345 
#define DEMI_PRODUCT_ID 67890 

void DemiUSBDriver::initialize(void) 
{ 
    IOReturn    result; 
    Int32     vendor_id = DEMI_VENDOR_ID; 
    Int32     product_id = DEMI_PRODUCT_ID; 
    mach_port_t    master_port; 
    CFMutableDictionaryRef matching_dict; 
    IONotificationPortRef notify_port; 
    CFRunLoopSourceRef  run_loop_source; 

    //create a master port 
    result = IOMasterPort(bootstrap_port, &master_port); 

    //set up a matching dictionary for the device 
    matching_dict = IOServiceMatching(kIOUSBDeviceClassName); 

    //add matching parameters 
    CFDictionarySetValue(matching_dict, CFSTR(kUSBVendorID), 
     CFNumberCreate(kCFAllocatorDefault, kCFNumberInt32Type, &vendor_id)); 
    CFDictionarySetValue(matching_dict, CFSTR(kUSBProductID), 
     CFNumberCreate(kCFAllocatorDefault, kCFNumberInt32Type, &product_id)); 

    //create the notification port and event source 
    notify_port = IONotificationPortCreate(master_port); 
    run_loop_source = IONotificationPortGetRunLoopSource(notify_port); 
    CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source, 
     kCFRunLoopDefaultMode); 

    //add an additional reference for a secondary event 
    // - each consumes a reference... 
    matching_dict = (CFMutableDictionaryRef)CFRetain(matching_dict); 

    //add a notification callback for detach event 
    //NOTE: removed_iter is a io_iterator_t, declared elsewhere 
    result = IOServiceAddMatchingNotification(notify_port, 
     kIOTerminatedNotification, matching_dict, device_detach_callback, 
     NULL, &removed_iter); 

    //call the callback to 'arm' the notification 
    device_detach_callback(NULL, removed_iter); 

    //add a notification callback for attach event 
    //NOTE: added_iter is a io_iterator_t, declared elsewhere 
    result = IOServiceAddMatchingNotification(notify_port, 
     kIOFirstMatchNotification, matching_dict, device_attach_callback, 
     NULL, &g_added_iter); 
    if (result) 
    { 
     throw Exception("Unable to add attach notification callback."); 
    } 

    //call the callback to 'arm' the notification 
    device_attach_callback(NULL, added_iter); 

    //'pump' the run loop to handle any previously added devices 
    service(); 
} 

有兩種方法在初始化代碼中用作回調:device_detach_callback和device_attach_callback(均在靜態方法中聲明)。 device_detach_callback很簡單:

//implementation 
void DemiUSBDevice::device_detach_callback(void* context, io_iterator_t iterator) 
{ 
    IOReturn  result; 
    io_service_t obj; 

    while ((obj = IOIteratorNext(iterator))) 
    { 
     //close all open resources associated with this service/device... 

     //release the service 
     result = IOObjectRelease(obj); 
    } 
} 

device_attach_callback是大部分魔術發生的地方。在我的代碼,我有這個分爲多種方法,但在這裏我將介紹它作爲一個大整體的方法...:

void DemiUSBDevice::device_attach_callback(void * context, 
    io_iterator_t iterator) 
{ 
    IOReturn     result; 
    io_service_t   usb_service; 
    IOCFPlugInInterface**  plugin; 
    HRESULT     hres; 
    SInt32      score; 
    UInt16      vendor; 
    UInt16      product; 
    IOUSBFindInterfaceRequest request; 
    io_iterator_t    intf_iterator; 
    io_service_t    usb_interface; 

    UInt8      interface_endpoint_count = 0; 
    UInt8      pipe_ref = 0xff; 

    UInt8      direction; 
    UInt8      number; 
    UInt8      transfer_type; 
    UInt16      max_packet_size; 
    UInt8      interval; 

    CFRunLoopSourceRef   m_event_source; 
    CFRunLoopSourceRef   compl_event_source; 

    IOUSBDeviceInterface245** dev = NULL; 
    IOUSBInterfaceInterface245** intf = NULL; 

    while ((usb_service = IOIteratorNext(iterator))) 
    { 
     //create the intermediate plugin 
     result = IOCreatePlugInInterfaceForService(usb_service, 
     kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin, 
     &score); 

     //get the device interface 
     hres = (*plugin)->QueryInterface(plugin, 
     CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID245), (void**)&dev); 

     //release the plugin - no further need for it 
     IODestroyPlugInInterface(plugin); 

     //double check ids for correctness 
     result = (*dev)->GetDeviceVendor(dev, &vendor); 
     result = (*dev)->GetDeviceProduct(dev, &product); 
     if ((vendor != DEMI_VENDOR_ID) || (product != DEMI_PRODUCT_ID)) 
     { 
     continue; 
     } 

     //set up interface find request 
     request.bInterfaceClass  = kIOUSBFindInterfaceDontCare; 
     request.bInterfaceSubClass = kIOUSBFindInterfaceDontCare; 
     request.bInterfaceProtocol = kIOUSBFindInterfaceDontCare; 
     request.bAlternateSetting = kIOUSBFindInterfaceDontCare; 

     result = (*dev)->CreateInterfaceIterator(dev, &request, &intf_iterator); 

     while ((usb_interface = IOIteratorNext(intf_iterator))) 
     { 
     //create intermediate plugin 
     result = IOCreatePlugInInterfaceForService(usb_interface, 
      kIOUSBInterfaceUserClientTypeID, kIOCFPlugInInterfaceID, &plugin, 
      &score); 

     //release the usb interface - not needed 
     result = IOObjectRelease(usb_interface); 

     //get the general interface interface 
     hres = (*plugin)->QueryInterface(plugin, CFUUIDGetUUIDBytes(
      kIOUSBInterfaceInterfaceID245), (void**)&intf); 

     //release the plugin interface 
     IODestroyPlugInInterface(plugin); 

     //attempt to open the interface 
     result = (*intf)->USBInterfaceOpen(intf); 

     //check that the interrupt endpoints are available on this interface 
     //calling 0xff invalid... 
     m_input_pipe = 0xff; //UInt8, pipe from device to Mac 
     m_output_pipe = 0xff; //UInt8, pipe from Mac to device 

     result = (*intf)->GetNumEndpoints(intf, &interface_endpoint_count); 
     if (!result) 
     { 
      //check endpoints for direction, type, etc. 
      //note that pipe_ref == 0 is the control endpoint (we don't want it) 
      for (pipe_ref = 1; pipe_ref <= interface_endpoint_count; pipe_ref++) 
      { 
      result = (*intf)->GetPipeProperties(intf, pipe_ref, &direction, 
       &number, &transfer_type, &max_packet_size, &interval); 
      if (result) 
      { 
       break; 
      } 

      if (transfer_type == kUSBInterrupt) 
      { 
       if (direction == kUSBIn) 
       { 
       m_input_pipe = pipe_ref; 
       } 
       else if (direction == kUSBOut) 
       { 
       m_output_pipe = pipe_ref; 
       } 
      } 
      } 
     } 

     //set up async completion notifications 
     result = (*m_intf)->CreateInterfaceAsyncEventSource(m_intf, 
      &compl_event_source); 
     CFRunLoopAddSource(CFRunLoopGetCurrent(), compl_event_source, 
      kCFRunLoopDefaultMode); 

     break; 
     } 

     break; 
    } 
} 

在這一點上,我們應該有中斷端點的數量和開放IOUSBInterfaceInterface到設備。數據的異步寫入可以通過調用像這樣做:

result = (intf)->WritePipeAsync(intf, m_output_pipe, 
      data, OUTPUT_DATA_BUF_SZ, device_write_completion, 
      NULL); 

,其中數據是寫的字符緩衝區,最後一個參數是一個可選的上下文對象傳遞給回調函數,並device_write_completion是一個靜態方法具有以下一般形式:

void DemiUSBDevice::device_write_completion(void* context, 
    IOReturn result, void* arg0) 
{ 
    //... 
} 

從中斷端點讀取是相似的:

result = (intf)->ReadPipeAsync(intf, m_input_pipe, 
      data, INPUT_DATA_BUF_SZ, device_read_completion, 
      NULL); 

其中device_read_completi在如下形式:

void DemiUSBDevice::device_read_completion(void* context, 
    IOReturn result, void* arg0) 
{ 
    //... 
} 

注意,要接收這些回調的運行循環必須運行(see this link for more information about the CFRunLoop)。達到此目的的一種方法是在調用異步讀取或寫入方法後調用CFRunLoopRun(),此時主線程在運行循環運行時阻塞。處理完回叫後,您可以撥打CFRunLoopStop(CFRunLoopGetCurrent())停止運行循環並將執行回執至主線程。

另一個替代方案(我在我的代碼中做的)是將一個上下文對象(在下面的代碼示例中名爲'request')傳遞給WritePipeAsync/ReadPipeAsync方法 - 該對象包含一個布爾完成標誌(名爲'is_done'在這個例子中)。調用讀/寫方法,而不是調用CFRunLoopRun()後,像下面這樣可以執行:

while (!(request->is_done)) 
{ 
    //run for 1/10 second to handle events 
    Boolean returnAfterSourceHandled = false; 
    CFTimeInterval seconds = 0.1; 
    CFStringRef mode = kCFRunLoopDefaultMode; 
    CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled); 
} 

這樣做的好處是,如果你有使用運行循環,你不會過早退出應其他線程另一個線程停止運行環...

我希望這是有幫助的人。我不得不從許多來源不全拉來解決這個問題,這需要大量的工作來獲得運行良好......

+0

+1。 +100這個答案很棒,我非常感謝你的辛勤工作。 – TarkaDaal 2014-05-08 09:50:27

2

閱讀這個問題了幾次,對這個問題考慮了一下之後,我認爲另一個解決方案,用於仿真阻止讀取行爲,但使用HID管理器而不是替換它。

一種阻塞讀取功能可以爲設備寄存器的輸入的回調,寄存器上當前運行迴路的裝置中,然後通過調用CFRunLoopRun塊()。輸入回調可以將報表複製到共享緩衝區並調用CFRunLoopStop(),這會導致CFRunLoopRun()返回,從而解除對read()的阻止。然後,read()可以將該報告返回給調用者。

我能想到的第一個問題就是該設備已經被調度上運行循環的情況。在讀取功能中調度然後取消調度設備可能會產生不利影響。但是,如果應用程序試圖在同一設備上使用同步和異步調用,那隻會是一個問題。

,想到的第二件事是在調用的代碼已經運行(可可和Qt的應用程序例如)運行循環的情況。但是,CFRunLoopStop()的文檔似乎表明,對CFRunLoopRun()的嵌套調用可以正確處理。所以,應該沒問題。

這裏有點簡化代碼走這一點。我只是在我的HID Library中實現了類似的東西,它似乎能夠工作,儘管我還沒有對其進行廣泛的測試。

/* An IN report callback that stops its run loop when called. 
    This is purely for emulating blocking behavior in the read() method */ 
static void input_oneshot(void*   context, 
          IOReturn  result, 
          void*   deviceRef, 
          IOHIDReportType type, 
          uint32_t  reportID, 
          uint8_t*  report, 
          CFIndex   length) 
{ 
    buffer_type *const buffer = static_cast<HID::buffer_type*>(context); 

    /* If the report is valid, copy it into the caller's buffer 
     The Report ID is prepended to the buffer so the caller can identify 
     the report */ 
    if(buffer) 
    { 
     buffer->clear(); // Return an empty buffer on error 
     if(!result && report && deviceRef) 
     { 
      buffer->reserve(length+1); 
      buffer->push_back(reportID); 
      buffer->insert(buffer->end(), report, report+length); 
     } 
    } 

    CFRunLoopStop(CFRunLoopGetCurrent()); 
} 

// Block while waiting for an IN interrupt report 
bool read(buffer_type& buffer) 
{ 
    uint8_t _bufferInput[_lengthInputBuffer]; 

    // Register a callback 
    IOHIDDeviceRegisterInputReportCallback(deviceRef, _bufferInput, _lengthInputBuffer, input_oneshot, &buffer); 

    // Schedule the device on the current run loop 
    IOHIDDeviceScheduleWithRunLoop(deviceRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 

    // Trap in the run loop until a report is received 
    CFRunLoopRun(); 

    // The run loop has returned, so unschedule the device 
    IOHIDDeviceUnscheduleFromRunLoop(deviceRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); 

    if(buffer.size()) 
     return true; 
    return false; 
} 
1

我碰到了這個相同的kIOReturnExclusiveAccess。而不是打擊它(建立kext等)。我找到了該設備並使用了POSIX API。

//My funcation was named differently, but I'm using this for continuity.. 
void DemiUSBDevice::device_attach_callback(void * context, 
    io_iterator_t iterator) 
{ 
DeviceManager *deviceManager = (__bridge DADeviceManager *)context; 
    io_registry_entry_t device; 
    while ((device = IOIteratorNext(iterator))) { 

    CFTypeRef prop; 
    prop = IORegistryEntrySearchCFProperty(device, 
              kIOServicePlane, 
              CFSTR(kIODialinDeviceKey), 
              kCFAllocatorDefault, 
              kIORegistryIterateRecursively); 
    if(prop){ 
     deviceManager->devPath = (__bridge NSString *)prop; 
     [deviceManager performSelector:@selector(openDevice)]; 
    } 
    } 
} 

一旦DEVPATH設置,你可以調用open和讀/寫..

int dfd; 
dfd = open([devPath UTF8String], O_RDWR | O_NOCTTY | O_NDELAY); 
    if (dfd == -1) { 
    //Could not open the port. 
    NSLog(@"open_port: Unable to open %@", devPath); 
    return; 
    } else { 
    fcntl(fd, F_SETFL, 0); 
    } 
+0

你是否在使用隱藏設備? – 2015-02-04 15:02:32