--- title: 第 13 章 USB设备 prev: books/arch-handbook/scsi next: books/arch-handbook/newbus showBookMenu: true weight: 15 params: path: "/books/arch-handbook/usb/" --- [[usb]] = USB设备 :doctype: book :toc: macro :toclevels: 1 :icons: font :sectnums: :sectnumlevels: 6 :sectnumoffset: 13 :partnums: :source-highlighter: rouge :experimental: :images-path: books/arch-handbook/ ifdef::env-beastie[] ifdef::backend-html5[] :imagesdir: ../../../../images/{images-path} endif::[] ifndef::book[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] toc::[] endif::[] ifdef::backend-pdf,backend-epub3[] include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] toc::[] include::../../../../../shared/asciidoctor.adoc[] endif::[] [[usb-intro]] == 简介 通用串行总线(USB)是将设备连接到个人计算机的一种新方法。总线 结构突出了双向通信的特色,并且其开发充分考虑到了设备正逐渐智能化 和需要与host进行更多交互的现实。对USB的支持包含在当前所有芯片中, 因此在新近制造的PC中都可用。苹果(Apple)引入仅带USB的iMac对硬件 制造商生产他们USB版本的设备是一个很大的激励。未来的PC规范指定 PC上的所有老连接器应当由一个或多个USB连接器取代,提供通用的 即插即用能力。对USB硬件的支持在NetBSD的相当早期就有了,它是由 Lennart Augustsson为NetBSD项目开发的。代码已经被移植到FreeBSD上, 我们目前维护着一个底层共享代码。对USB子系统的实现来说,许多USB的 特性很重要。 _Lennart Augustsson已经完成了NetBSD项目中USB支持的 大部分实现。十分感谢这项工作量惊人的工作。也十分感谢Ardy和Dirk 对本文稿的评论和校对。_ * 设备直接连接到计算机上的端口,或者连接到称为 集中器的设备,形成树型设备结构。 * 设备可在运行时连接或断开。 * 设备可以挂起自身并触发host系统的重新投入运行。 * 由于设备可由总线供电,因此host软件必须跟踪每个 集中器的电源预算。 * 不同设备类型需要不同的服务质量,并且同一总线 可以连接最多126个设备,这就需要恰当地调度总线上的传输以充分 利用12Mbps的可用带宽。(USB 2.0超过400Mbps) * 设备智能化并包含很容易访问到的关于自身的信息。 为USB子系统以及连接到它的设备开发驱动程序受已开发或将要开发的 规范的支持。这些规范可以从USB主页公开获得。苹果(Apple)通过使得 通用类驱动程序可从其操作系统MacOS中获得,而且不鼓励为每种新设备 使用单独的驱动程序来强烈推行基于标准的驱动程序。本章试图整理基本 信息以便对FreeBSD/NetBSD中USB栈的当前实现有个基本的了解。然而, 建议将下面参考中提及的相关规范与本章同时阅读。 === USB栈的结构 FreeBSD中的USB支持可被分为三层。最底层包含主控器,向硬件 及其调度设施提供一个通用接口。它支持硬件初始化,对传输进行调度, 处理已完成/失败的传输。每个主控器驱动程序实现一个虚拟hub, 以硬件无关方式提供对控制机器背面根端口的寄存器的访问。 中间层处理设备连接和断开,设备的基本初始化,驱动程序的选择, 通信通道(管道)和资源管理。这个服务层也控制默认管道和其上传输的 设备请求。 顶层包含支持特定(类)设备的各个驱动程序。这些驱动程序实现 除默认管道外的其他管道上使用的协议。他们也实现额外功能,使得设备 对内核或用户空间是可见的。他们使用服务层暴露出的USB驱动程序接口 (USBDI)。 [[usb-hc]] == 主控器 主控器(HC)控制总线上包的传输。使用1毫秒的帧。在每帧开始 时,主控器产生一个帧开始(SOF, Start of Frame)包。 SOF包用于同步帧的开始和跟踪帧的数目。包在帧中被传输,或由host 到设备(out),或由设备到host(in)。传输总是由host发起(轮询传输)。 因此每条USB总线只能有一个host。每个包的传输都有一个状态阶段, 数据接收者可以在其中返回ACK(应答接收),NAK(重试),STALL(错误 条件)或什么也没有(混乱数据阶段,设备不可用或已断开)。USB规范 http://www.usb.org/developers/docs.html[USB specification]的第8.5节更详细地解释了包的细节。USB总线 上可以出现四中不同类型的传输:控制(control), 大块(bulk), 中断 (interrupt)和同步(isochronous)。传输的类型和他们的特性在下面 描述(管道'子节中)。 USB总线上的设备和设备驱动程序间的大型传输被主控器或HC 驱动程序分割为多个包。 到默认端点的设备请求(控制传输)有些特殊。它们由两或三个阶段 组成:启动(SETUP),数据(DATA,可选)和状态(STATUS)。设置(set-up) 包被发送到设备。如果存在数据阶段,数据包的方向在设置包中给出。 状态阶段中的方向与数据阶段期间的方向相反,或者当没有数据阶段时 为IN。主控器硬件也提供寄存器,用于保存根端口的当前状态和自从 状态改变寄存器最后一次复位以来所发生的改变。USB规范[2]建议使用一个 虚拟hub来提供对这些寄存器的访问。虚拟hub必须符合规范第11章中给出的 hub设备类。它必须提供一个默认管道使得设备请求可以发送给它。它返回 标准和hub类特定的一组描述符。它也应当提供一个中断管道用来报告其 端口发生的变化。当前可用的主控器规范有两个: http://developer.intel.com/design/USB/UHCI11D.htm[ 通用主控器接口](UHCI;英特尔)和link:http://www.compaq.com/productinfo/development/openhci.html[开放主控器接口](OHCI;康柏,微软,国家半导体)。 UHCI规范的设计通过要求主控器驱动程序为每帧的传输提供完整的调度, 从而减少了硬件复杂性。OHCI类型的控制器自身提供一个更抽象的接口来 完成很多工作,从而更加独立。 === UHCI UHCI主控器维护着带有1024个指向每帧数据结构的帧列表。 它理解两种不同的数据类型:传输描述符(TD)和队列头(QH)。每个 TD表示表示与设备端点进行通信的一个包。QH是将一些TD(和QH)划分 成组的一种方法。 每个传输由一个或多个包组成。UHCI驱动程序将大的传输分割成 多个包。除同步传输外,每个传输都会分配一个QH。对于每种类型的 传输,都有一个与此类型对应的QH,所有这些QH都会被集中到这个QH上。 由于有固定的时延需求,同步传输必须首先执行,它是通过帧列表中的 指针直接引用的。最后的同步TD传输引用那一帧的中断传输的QH。中断 传输的所有QH指向控制传输的QH,控制传输的QH又指向大块传输的QH。 下面的图表给出了一个图形概览: 这导致下面的调度会在每帧中运行。控制器从帧列表中取得当前帧 的指针后,首先为那一帧中的所有的同步(isochronous)包执行TD。 这些TD的最后一个 引用那一帧的中断传输的QH。然后主控器将从那个QH下行到各个 中断传输的QH。完成那一队列后,中断传输的QH会将控制器指向到所有 控制传输的QH。它将执行在那儿等待调度的所有子队列,然后是在大块QH中 排队的所有传输。为了方便处理已完成或失败的传输,硬件会在每帧末尾 产生不同类型的中断。在传输的最后一个TD中,HC驱动程序设置 Interrupt-On-Completion位来标记传输完成时的一个中断。如果TD达到了 其最大错误数,就标记错误中断。如果在TD中设置短包侦测位,且传输了 小于所设置的包长度(的包),就会标记此中断以通知控制器驱动程序传输 已完成。找出哪个传输已完成或产生错误是主控器驱动程序的任务。 当中断服务例程被调用时,它将定位所有已完成的传输并调用它们的回调。 更详尽的描述请看 http://developer.intel.com/design/USB/UHCI11D.htm[UHCI specification。] === OHCI 对OHCI主控器进行编程要容易得多。控制器假设有一组端点(endpoint)可用, 并知道帧中不同传输类型的调度优先级和排序。主控器使用的主要 数据结构是端点描述符(ED),它上面连接着一个传输描述符(TD)的队列。 ED包含端点所允许的最大的包大小,控制器硬件完成包的分割。每次传输 后都会更新指向数据缓冲区的指针,当起始和终止指针相等时,TD就退归 到完成队列(done-queue)。四种类型的端点各有其自己的队列。控制和 大块(bulk)端点分别在它们自己的队列排队。中断ED在树中排队,在树中的深度 定义了它们运行的频度。 帧列表 中断 同步(isochronous) 控制 大块(bulk) 主控器在每帧中运行的调度看起来如下。控制器首先运行非 周期性控制和大块队列,最长可到HC驱动程序设置的一个时间限制。 然后以帧编号低5位作为中断ED树上深度为0的那一层中的索引,运行 那个帧编号的中断传输。在这个树的末尾,同步ED被连接,并随后被 遍历。同步TD包含了传输应当运行其中的第一个帧的帧编号。所有周期 性的传输运行过以后,控制和大块队列再次被遍历。中断服务例程会被 周期性地调用,来处理完成的队列,为每个传输调用回调,并重新调度 中断和同步端点。 更详尽的描述请看 http://www.compaq.com/productinfo/development/openhci.html[ OHCI specification]。服务层,即中间层,提供了以可控的方式 对设备进行访问,并维护着由不同驱动程序和服务层所使用的资源。 此层处理下面几方面: * 设备配置信息 * 与设备进行通信的管道 * 探测和连接设备,以及从设备分离(detach)。 [[usb-dev]] == USB设备信息 === 设备配置信息 每个设备提供了不同级别的配置信息。每个设备具有一个或多个 配置,探测/连接期间从其中选定一个。配置提供功率和带宽要求。 每个配置中可以有多个接口。设备接口是端点的汇集(collection)。 例如,USB扬声器可以有一个音频接口(音频类),和对旋钮(knob)、 拨号盘(dial)和按钮的接口(HID类)。 一个配置中的所有接口可以同时有效,并可被不同的 驱动程序连接。每个接口可以有备用接口,以提供不同质量的服务参数。 例如,在照相机中,这用来提供不同的帧大小以及每秒帧数。 每个接口中可以指定0或多个端点。端点是与设备进行通信的单向 访问点。它们提供缓冲区来临时存储从设备而来的,或外出到设备的数据。 每个端点在配置中有唯一地址,即端点号加上其方向。默认端点,即 端点0,不是任何接口的一部分,并且在所有配置中可用。它由服务层 管理,并且设备驱动程序不能直接使用。 Level 0 Level 1 Level 2 Slot 0 Slot 3 Slot 2 Slot 1 (只显示了32个槽中的4个) 这种层次化配置信息在设备中通过标准的一组描述符来描述(参看 USB规范[2]第9.6节)。它们可以通过Get Descriptor Request来请求。 服务层缓存这些描述符以避免在USB总线上进行不必要的传输。对这些 描述符的访问是通过函数调用来提供的。 * 设备描述符:关于设备的通用信息,如供应商,产品 和修订ID,支持的设备类、子类和适用的协议,默认端点的最大包大小 等。 * 配置描述符:此配置中的接口数,支持的挂起和 恢复能力,以及功率要求。 * 接口描述符:接口类、子类和适用的协议,接口备用 配置的数目和端点数目。 * 端点描述符:端点地址、方向和类型,支持的最大包 大小,如果是中断类型的端点则还包括轮询频率。默认端点(端点0) 没有描述符,而且从不被计入接口描述符中。 * 字符串描述符:在其他描述符中会为某些字段提供 字符串索引。它们可被用来检取描述性字符串,可能以多种语言 的形式提供。 类说明(specification)可以添加它们自己的描述符类型,这些描述符 也可以通过GetDescriptor Request来获得。 管道与设备上端点的通信,流经所谓的管道。驱动程序将到端点的 传输提交到管道,并提供传输(异步传输)失败或完成时调用的回调, 或等待完成(同步传输)。到端点的传输在管道中被串行化。传输或者完成, 或者失败,或者超时(如果设置了超时)。对于传输有两种类型的超时。 超时的发生可能由于USB总线上的超时(毫秒)。这些超时被视为失败, 可能是由于设备断开连接引起的。另一种超时在软件中实现,当传输没有 在指定的时间(秒)内完成时触发。这是由于设备对传输的包否定应答引起的。 其原因是由于设备还没有准备好接收数据,缓冲区欠载或超载,或协议错误。 如果管道上的传输大于关联的端点描述符中指定的最大包大小,主 控器(OHCI)或HC驱动程序(UHCI)将按最大包大小分割传输,并且最后 一个包可能小于最大包的大小。 有时候对设备来说返回少于所请求的数据并不是个问题。例如, 到调制解调器的大块in传输可能请求200字节的数据,但调制解调器 那时只有5个字节可用。驱动程序可以设置短包(SPD)标志。它允许主 控器即使在传输的数据量少于所请求的数据量的情况下也接受包。 这个标志只在in传输中有效,因为将要被发送到设备的数据量总是事先 知道的。如果传输过程中设备出现不可恢复的错误,管道会被停顿。 接受或发送更多数据以前,驱动程序需要确定停顿的原因,并通过在 默认管道上发送清除端点挂起设备请求(clear endpoint halt device request)来清除端点停顿条件。 有四种不同类型的端点和对应的管道: - * 控制管道/默认管道: 每个设备有一个控制管道,连接到默认端点(端点0)。此管道运载设备 请求和关联的数据。默认管道和其他管道上的传输的区别在于传输所 使用的协议,协议在USB规范[2]中描述。这些请求用于复位和配置设备。 每个设备必须支持USB规范[2]的第9章中提供的一组基本命令。管道上 支持的命令可以通过设备类规范扩展,以支持额外的功能。 * 大块(bulk)管道:这是USB与原始传输媒体对应的等价物。 * 中断管道:host向设备发送数据请求,如果设备没有 东西发送,则将NAK(否定应答)数据包。中断传输按创建管道时指定的 频率被调度。 * 同步管道:这些管道用于具有固定时延的同步数据, 例如视频或音频流,但不保证一定传输。当前实现中已经有对这种类型 管道的某些支持。当传输期间出现错误,或者由于,例如缺乏缓冲区空间 来存储进入的数据而引起的设备否定应答包(NAK)时,控制、大块和中断 管道中的包会被重试。而同步包在传递失败或对包NAK时不会重试,因为 那样可能违反同步约束。 所需带宽的可用性在管道的创建期间被计算。传输在1毫秒的帧内 进行调度。帧中的带宽分配由USB规范的第5.6节规定。同步和中断传输被 允许消耗帧中多达90%的带宽。控制和大块传输的包在所有同步和中断包 之后进行调度,并将消耗所有剩余带宽。 关于传输调度和带宽回收的更多信息可以在USB规范[2]的第5章, UHCI规范[3]的的第1.3节,OHCI规范[4]的3.4.2节中找到。 [[usb-devprobe]] == 设备的探测和连接 集中器(hub)通知新设备已连接后,服务层给端口加电(switch on), 为设备提供100mA的电流。 此时设备处于其默认状态,并监听设备地址0。服务层会通过默认 管道继续检取各种描述符。此后它将向设备发送Set Address请求,将设备 从默认设备地址(地址0)移开。可能有多个设备驱动程序支持此设备。例如, 一个调制解调器可能通过AT兼容接口支持ISDN TA。然而,特定型号的ISDN 适配器的驱动程序可能提供对此设备的更好支持。为了支持这样的灵活性, 探测会返回优先级,指示他们的支持级别。支持产品的特定版本会具有最高 优先级,通用驱动程序具有最低优先级。如果一个配置内有多个接口,也可能 多个驱动程序会连接到一个设备。每个驱动程序只需支持所有接口的一个子集。 为新连接的设备探测驱动程序时,首先探测设备特定的驱动程序。 如果没有发现,则探测代码在所有支持的配置上重复探测过程,直到 在一个配置中连接到一个驱动程序。为了支持不同接口上使用多个驱动 程序的设备,探测会在一个配置中的所有尚未被驱动程序声明(claim)的 接口上重复进行。超出集中器功率预算的配置会被忽略。连接期间,驱动 程序应当把设备初始化到适当状态,但不能复位,因为那样会使得设备将 它自己从总线上断开,并重新启动探测过程。为了避免消耗不必要的带宽, 不应当在连接时声明中断管道,而应当延迟分配管道,直到打开文件并真的 使用数据。当关闭文件时,管道也应当被再次关闭,尽管设备可能仍然 连接着。 === 设备断开连接(disconnect)和分离(detach) 设备驱动程序与设备进行任何事务期间,应当预期会接收到错误。 USB的设计支持并鼓励设备在任何点及时断开连接。驱动程序应当确保 当设备不在时做正确的事情。 此外,断开连接(disconnect)后又重新连接(reconnect)的设备不会 被重新连接(reattach)为相同的设备实例。 将来当更多的设备支持序列号(参看设备描述符), 或开发出其他定义设备标识的方法的时候,这种情况可能会改变。 设备断开连接是由集中器在传递到集中器驱动程序的中断包中发 信号通知(signal)的。状态改变信息指示哪个端口发现了连接改变。 连接到那个端口上的设备的所有设备驱动程序共用的设备分离方法被调用, 结构被彻底清理。如果端口状态指示同时一个设备已经连接(connect)到那个 端口,则探测和连接设备的过程将被启动。设备复位将在集中器上产生 一个断开-连接序列,并将按上面所述进行处理。 [[usb-protocol]] == USB驱动程序的协议信息 USB规范没有定义除默认管道外其他管道上使用的协议。这方面的信息 可以从各种来源获得。最准确的来源是USB主页[1]上的开发者部分。从这些 页面上可以得到数目不断增长的设备类的规范。这些规范指定从驱动程序 角度看起来兼容设备应当怎样,它需要提供的基本功能和通信通道上使用的 协议。USB规范[2]包括了集中器类的描述。人机界面设备(HID)的类规范已经 创建出来,以迎合对键盘、数字输入板、条形码阅读器、按钮、旋钮(手柄knob)、 开关等的要求。另一个例子是用于大容量存储设备的类规范。设备类的完整列表 参看USB主页[1]的开发者部分。 然而, 许多设备的协议信息还没有被公布。关于所用协议的信息 可能可以从制造设备的公司获得。一些公司会在给你规范之前要求你签署 保密协议(Non-Disclosure Agreement, NDA)。大多数情况下,这会阻止 将驱动程序开放源代码。 另一个信息的很好来源是Linux驱动程序源代码,因为很多公司已经 开始为他们的设备提供Linux下的驱动程序。联系那些驱动程序作者询问 他们的信息来源总是一个好主意。 例子:人机界面设备。人机界面设备,如键盘、鼠标、数字输入板、 按钮、拨号盘等的规范被其他设备类规范引用,并在很多设备中使用。 例如,音频扬声器提供到数模转换器的端点,可能还提供额外管道 用于麦克风。它们也为设备前面的按钮和拨号盘在单独的接口中提供HID 端点。监视器控制类也是如此。通过可用的内核和用户空间的库,与HID 类驱动程序或通用驱动程序一起可以简单直接地创建对这些接口的支持。 另一个设备可以作为在一个配置中的多个接口由不同的设备驱动程序驱动 的例子,这个设备是一种便宜的键盘,带有老的鼠标接口。为了避免在 设备中为USB集中器包括一个硬件而导致的成本上升,制造商将从键盘背面的 PS/2端口接收到的鼠标数据与来自键盘的按键组合成在同一个配置中的 两个单独的接口。鼠标和键盘驱动程序各自连接到适当的接口,并分配到 两个独立端点的管道. 例子:固件下载。已经开发出来的许多设备是基于通用目的处理器, 并将额外的USB核心加入其中。由于驱动程序的开发和USB设备的固件仍然 非常新,许多设备需要在连接(connect)之后下载固件。 下面的步骤非常简明直接。设备通过供应商和产品ID标识自身。第一 个驱动程序探测并连接到它,并将固件下载到其中。此后设备自己软复位, 驱动程序分离。短暂的暂停之后设备宣布它在总线上的存在。设备将改变 其供应商/产品/版本的ID以反映其提供有固件的事实,因此另一个驱动程序 将探测它并连接(attach)到它。 这些类型的设备的一个例子是基于EZ-USB的ActiveWire I/O板。这个 芯片有一个通用固件下载器。下载到ActiveWire板子上的固件改变版本ID。 然后它将执行EZ-USB芯片的USB部分的软复位,从USB总线上断开,并再次 重新连接。 例子:大容量存储设备。对大容量存储设备的支持主要围绕现有的 协议构建。Iomega USB Zip驱动器是基于SCSI版本的驱动器。SCSI命令和 状态信息被包装到块中,在大块(bulk)管道上传输到/来自设备,在USB线 上模拟SCSI控制器。ATAPI和UFI命令以相似的方式被支持。 大容量存储规范支持两种不同类型的对命令块的包装。最初的尝试 基于通过默认管道发送命令和状态信息,使用大块传输在host和设备之间 移动数据。在经验基础上设计出另一种方法,这种方法基于包装命令和 状态块,并在大块out和in端点上发送它们。规范精确地指定了何时必须 发生什么,以及在碰到错误条件的情况下应该做什么。为这些设备编写 驱动程序的最大挑战是协调基于USB的协议,让它适合已有的对大容量存储设备 的支持。CAM提供了钩子,以相当直接了当的方式来完成这个。ATAPI就 没有这么简单了,因为历史上IDE接口从未有过多种不同的表现方式。 来自Y-E Data的对USB软盘的支持也不是那么直观,因为设计了一套 新的命令集。