在 Mac 上读写 USB(HID)中断端点

22

我正在尝试与一个特定的USB设备通信,并开发既适用于Windows又适用于Mac的代码。

该设备是具有两个端点(中断输入和中断输出)的HID接口(类3)USB设备。设备的性质是仅当主机请求数据时,才会从设备上的输入端点发送数据:主机向其输入中断端点发送数据,设备则作出响应。将数据传输到设备(写入)要简单得多......

Windows的代码相当简单:我获取设备的句柄,然后调用ReadFile或WriteFile即可。许多底层异步行为似乎被抽象出来了。它似乎正常工作。

然而,在Mac上,情况有些棘手。我尝试了许多方法,但没有一个完全成功,但以下两种方法似乎最有希望:

1.)尝试通过IOUSBInterfaceInterface(作为USB)访问设备,迭代端点以确定输入和输出端点,并(希望)使用ReadPipe和WritePipe进行通信。不幸的是,一旦得到接口,我就无法打开它,返回值(kIOReturnExclusiveAccess)指出已经有其他程序独占地打开了该设备。我尝试使用IOUSBinterfaceInterface183,这样我就可以调用USBInterfaceOpenSeize,但结果仍然返回相同的错误值。

--- 2010年7月30日更新 ---
显然,Apple IOUSBHIDDriver会早期匹配设备,这可能会阻止打开IOUSBInterfaceInterface。经过一些研究,似乎防止IOUSBHIDDriver匹配的常见方法是编写一个具有更高探测分数的无代码kext(内核扩展)。这将早期匹配,防止IOUSBHIDDriver打开设备,并在理论上允许我直接打开接口并读取或写入端点。这样做也可以,但我非常不想在用户机器上安装其他东西。如果有人知道可靠的替代方法,我会感激提供信息。

2.) 打开设备作为IOHIDDeviceInterface122(或更高版本)。 为了读取数据,我设置了一个异步端口、事件源和回调方法,当数据准备好时会被调用,即从输入中断端点发送数据时。 然而,要写入设备需要的数据以初始化响应,我找不到方法。 我被卡住了。通常setReport会写入控制端点,但我需要一种不期望任何直接响应且不阻塞的写入方法。

我在网上找了很多东西并尝试了许多方法,但都没有成功。 有什么建议吗? 由于我必须在10.4上使用应用程序,因此我不能使用太多的Apple HIDManager代码,因为大部分都是10.5+。

3个回答

36
我现在有一个可工作的Mac驱动程序,用于需要通过中断端点进行通信的USB设备。以下是我的做法:
最终对我有效的方法是选项1(如上所述)。正如所指出的,我在打开COM-style IOUSBInterfaceInterface到设备时遇到了问题。随着时间的推移,很明显这是由于HIDManager捕获了该设备。一旦被捕获,我无法从HIDManager那里夺回设备的控制权(即使是USBInterfaceOpenSeize调用或USBDeviceOpenSeize调用也不起作用)。
为了控制设备,我需要在HIDManager之前抓住它。解决此问题的方法是编写一个无代码kext(内核扩展)。Kext本质上是一个包,位于System / Library / Extensions中,其中包含(通常)一个plist(属性列表)和(偶尔)一个内核级驱动程序,以及其他项目。在我的情况下,我只想要plist,它将向内核提供有关其匹配的设备的说明。如果数据比HIDManager给出更高的探测分数,则我可以捕获设备并使用用户空间驱动程序与其通信。
编写的kext 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. 更改所有者为root:wheel (sudo chown root:wheel DemiUSBDevice.kext)
  2. 将kext复制到Extensions (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);

其中,data是要写入的字符缓冲区,最后一个参数是可选的上下文对象,将传递给回调函数。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_completion的形式如下:

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

请注意,要接收这些回调,运行循环必须正在运行(有关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);
}

这样做的好处是,如果你有其他使用运行循环的线程,当另一个线程停止运行循环时,你不会过早地退出...
我希望这对人们有所帮助。我不得不从许多不完整的来源中获取信息来解决这个问题,这需要相当大的工作量才能使其正常运行...

3
这个答案非常出色,我非常感谢你的辛勤工作。+1点赞,+100满分。 - TarkaDaal

2
我遇到了相同的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和read / write操作。

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);
  }

2
阅读这个问题几次并思考了一会儿后,我想到了另一种解决方案,可以模拟阻塞读取行为,但使用HID管理器而不是替换它。
一个阻塞读取函数可以为设备注册输入回调,将设备注册到当前运行循环中,并通过调用CFRunLoopRun()来阻塞。输入回调可以将报告复制到共享缓冲区中并调用CFRunLoopStop(),这会导致CFRunLoopRun()返回,从而取消阻塞read()。然后,read()可以将报告返回给调用者。
我能想到的第一个问题是设备已经在运行循环上安排的情况。在读取函数中安排并取消安排设备可能会产生不良影响。但是只有当应用程序尝试在同一设备上同时使用同步和异步调用时才会出现问题。
第二件事是调用代码已经有一个运行循环在运行的情况(例如Cocoa和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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接