天天看点

usb4java源码分析usb协议操作流程如何收发字节数据

文章目录

  • usb协议
  • 操作流程
  • 如何收发字节数据

usb协议

可以简单认为host拥有一棵usb树,在这棵树上挂载usb节点。查询和连接指定的device时需要给出两个信息:

  • vendorId
  • productId

因此对于high-level api,有以下查找device的实现:

// 和递归查目录树是一样的
public UsbDevice findDevice(UsbHub hub, short vendorId, short productId)
{
    for (UsbDevice device : (List<UsbDevice>) hub.getAttachedUsbDevices())
    {
        UsbDeviceDescriptor desc = device.getUsbDeviceDescriptor();
        if (desc.idVendor() == vendorId && desc.idProduct() == productId) return device;
        if (device.isUsbHub())
        {
            device = findDevice((UsbHub) device, vendorId, productId);
            if (device != null) return device;
        }
    }
    return null;
}
           

操作流程

使用usb4java的通信流程如下:

graph TD
A[find device]
A-->B[claim iface]
B-->C{get endpoint}
C-->|UsbConst.OUT| D[outPipe]
C-->|UsbConst.IN| E[inPipe]
D-->F[open pipe]
E-->F[open pipe]
F-->G[syn- or asyn- submit byte data]
G-->H[close pipe]
H-->I[release iface]
           

上述endpoint之后分出in和out两种pipe,与usb的位定义有关。具体地,endpoint有一个字节的描述符,其中0:3位表示usb number;4:6位保留,第7位指示是发送还是接收。因此通过指定第7位的值(使用UsbConst)来获取in/out pipe。

如何收发字节数据

当pipe打开后,有两种方式收发数据:同步和异步submit。以下主要介绍异步submit是如何收发数据的。

首先submit需要传入一个byte数组:

byte[] data = new byte[1024];
inPipe.asyncSubmit(data);
           

该数组会被UsbIrp对象包装,然后塞到一个IrpQueue中。

final UsbIrp irp = createUsbIrp();
irp.setAcceptShortPacket(true);
irp.setData(data);
...
this.queue.add(irp);
           

队列检测到有包进入,启用一个线程处理:

public final void add(final T irp)
{
    this.irps.add(irp);

    // Start the queue processor if not already running.
    if (this.processor == null)
    {
        this.processor = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                process();
            }
        });
        this.processor.setDaemon(true);
        this.processor.setName("usb4java IRP Queue Processor");
        this.processor.start();
    }
}
           

具体处理步骤如下:

final void process()
{
    // Get the next IRP
    T irp = this.irps.poll();
    
    // If there are no IRPs to process then mark the thread as closing
    // right away. Otherwise process the IRP (and more IRPs from the queue
    // if present).
    if (irp == null)
    {
        this.processor = null;
    }
    else
    {
        while (irp != null)
        {
            // Process the IRP
            try
            {
                processIrp(irp);
            }
            catch (final UsbException e)
            {
                irp.setUsbException(e);
            }

            // Get next IRP and mark the thread as closing before sending
            // the events for the previous IRP
            final T nextIrp = this.irps.poll();
            if (nextIrp == null) this.processor = null;

            // Finish the previous IRP
            irp.complete();
            finishIrp(irp);

            // Process next IRP (if present)
            irp = nextIrp;
        }
    }

    // No more IRPs are present in the queue so terminate the thread.
    synchronized (this.irps)
    {
        this.irps.notifyAll();
    }
}
           

可以看到,在处理一个Irp之后,线程会调用finish(irp)方法,该方法实际是在发送一个事件给监听者。

protected void finishIrp(final UsbIrp irp)
{
    this.pipe.sendEvent(irp);
}

public void sendEvent(final UsbIrp irp)
{
    if (irp.isUsbException())
    {
        this.listeners.errorEventOccurred(new UsbPipeErrorEvent(this, irp));
    }
    else
    {
        this.listeners.dataEventOccurred(new UsbPipeDataEvent(this, irp));
    }
}
           

现在你就知道为什么官网的Guide上说使用异步submit要添加一个监听器了。

到这里,我们还想深入探究下process方法中的processIrp具体是怎么工作的,它是不是irp包没装满就返回呢?

protected void processIrp(final UsbIrp irp) throws UsbException
{
    final UsbEndpoint endpoint = this.pipe.getUsbEndpoint();
    final byte direction = endpoint.getDirection();
    final byte type = endpoint.getType();
    if (type == UsbConst.ENDPOINT_TYPE_CONTROL)
    {
        processControlIrp((UsbControlIrp) irp);
        return;
    }

    switch (direction)
    {
        case UsbConst.ENDPOINT_DIRECTION_OUT: // 发送
            irp.setActualLength(write(irp.getData(), irp.getOffset(),
                irp.getLength()));
            if (irp.getActualLength() < irp.getLength()
                && !irp.getAcceptShortPacket())
            {
                throw new UsbShortPacketException();
            }
            break;

        case UsbConst.ENDPOINT_DIRECTION_IN: // 接收
            irp.setActualLength(read(irp.getData(), irp.getOffset(),
                irp.getLength()));
            if (irp.getActualLength() < irp.getLength()
                && !irp.getAcceptShortPacket())
            {
                throw new UsbShortPacketException();
            }
            break;

        default:
            throw new UsbException("Invalid direction: "
                + direction);
    }
}
           

我们好好琢磨下switch中的两个case,一个对应发送,一个对应接收。这两个分支分别调用了write和read方法。

private int read(final byte[] data, final int offset, final int len) throws UsbException
{
    final UsbEndpointDescriptor descriptor = getEndpointDescriptor();
    final byte type = this.pipe.getUsbEndpoint().getType();
    final DeviceHandle handle = getDevice().open();
    int read = 0;
    while (read < len)
    {
        final int size =
            Math.min(len - read, descriptor.wMaxPacketSize() & 0xffff);
        final ByteBuffer buffer = ByteBuffer.allocateDirect(size); // 采用nio的方法
        final int result = transfer(handle, descriptor, type, buffer);
        buffer.rewind();
        buffer.get(data, offset + read, result);
        read += result;

        // Short packet detected, aborting
        if (result < size) break;
    }
    return read;
}
           

这里说明一下short packet:

在使用irp时,可以设置irp是否使用short packet模式,如果设置了,则显然根据上述代码可以推断,irp会在未装满数据的情况下返回,并且触发finish事件,然后进入下一个irp的处理。如果没有设置short packet呢?在没装满数据时while循环条件会一直不满足,所以线程应该会阻塞。

read代码是这样的,那write代码short packet是怎么工作的呢?

private int write(final byte[] data, final int offset, final int len) throws UsbException
{
    final UsbEndpointDescriptor descriptor = getEndpointDescriptor();
    final byte type = this.pipe.getUsbEndpoint().getType();
    final DeviceHandle handle = getDevice().open();
    int written = 0;
    while (written < len)
    {
        final int size =
            Math.min(len - written, descriptor.wMaxPacketSize() & 0xffff);
        final ByteBuffer buffer = ByteBuffer.allocateDirect(size);
        buffer.put(data, offset + written, size);
        buffer.rewind();
        final int result = transfer(handle, descriptor, type, buffer);
        written += result;

        // Short packet detected, aborting
        if (result < size) break;
    }
    return written;
}
           

同样的,在write操作中,没开启short packet的话,如果需要发送的信息小于packet最大尺寸,信息会发出去,但是线程在发送完信息后会阻塞,这样就不会触发finish事件。

这样会有什么问题呢?想象一下线程一直卡在一个irp包上,后续submit的包就不会受理,也就是后续信息发不出去,这并不是我们想要的。所以应该始终开启short packet模式。

开启short packet后需要注意的是,在处理接收事件时,一定要根据irp的actualLength来读data,而不是length,因为该包很可能没装满。

usb4java默认始终开启short packet模式。