第 13 章 USB 设备
最后更新于
最后更新于
通用串行总线(USB)是一种将设备连接到个人计算机的新方式。该总线架构具有双向通信功能,是对设备变得更加智能并且需要与主机更多交互的响应。USB 支持包含在所有当前的 PC 芯片组中,因此所有新建的 PC 都支持 USB。苹果公司推出的仅支持 USB 的 iMac 是硬件制造商生产 USB 版本设备的重要推动力。未来的 PC 规格要求将 PC 上的所有传统连接器替换为一个或多个 USB 连接器,提供通用的即插即用功能。NetBSD 早期就提供了 USB 硬件支持,并由 Lennart Augustsson 为 NetBSD 项目开发。该代码已经移植到 FreeBSD,目前我们正在维护一个共享的代码库。对于 USB 子系统的实现,USB 的若干特性非常重要。
Lennart Augustsson 完成了大部分 NetBSD 项目的 USB 支持实现。非常感谢他付出的大量工作。还要特别感谢 Ardy 和 Dirk 对本文的评论和校对。
设备通过计算机的端口或叫做集线器的设备连接,形成一个树形的设备结构。
设备可以在运行时连接和断开。
设备可以自我挂起,并触发主机系统的恢复。
由于设备可以从总线供电,主机软件必须跟踪每个集线器的电源预算。
由于不同设备类型具有不同的服务质量要求,加上同一总线上最多可以连接 126 个设备,因此需要对共享总线上的传输进行适当调度,以充分利用可用的 12 Mbps 带宽。(USB 2.0 可达到 400 Mbps)
设备是智能的,包含可以轻松访问的关于它们自身的信息。
USB 子系统及其连接设备的驱动程序开发得到了已经制定和即将制定的规格的支持。这些规格可从 USB 网站公开获取。苹果公司在推动基于标准的驱动程序方面做得非常好,提供了他们操作系统 MacOS 中用于通用类设备的驱动程序,并且不鼓励为每个新设备使用单独的驱动程序。本章试图汇总一些基本的 USB 2.0 实现栈在 FreeBSD/NetBSD 中的关键信息。然而,建议将其与相关的 2.0 规格和其他开发者资源一起阅读:
USB 2.0 规格()
通用主机控制器接口(UHCI)规格()
开放主机控制器接口(OHCI)规格()
USB 网站的开发者部分()
FreeBSD 中的 USB 支持可以分为三层。最底层包含主机控制器驱动程序,提供硬件和调度设施的通用接口。它支持硬件初始化、传输调度和已完成或失败传输的处理。每个主机控制器驱动程序实现一个虚拟集线器,提供对机器背面根端口控制寄存器的硬件独立访问。
中间层处理设备的连接和断开、设备的基本初始化、驱动程序选择、通信通道(管道)以及资源管理。该服务层还控制默认管道和通过这些管道传输的设备请求。
最上层包含支持特定(类)设备的各个驱动程序。这些驱动程序实现了在除默认管道外的管道上使用的协议。它们还实现了其他功能,使设备可以被内核或用户空间的其他部分使用。它们使用服务层暴露的 USB 驱动程序接口(USBDI)。
主机控制器(HC)控制总线上数据包的传输。每帧为 1 毫秒。在每帧开始时,主机控制器会生成一个帧开始(SOF)数据包。
SOF 数据包用于同步帧的开始,并跟踪帧编号。在每帧内,数据包要么从主机传输到设备(out),要么从设备传输到主机(in)。传输始终由主机发起(轮询传输)。因此,每个 USB 总线上只能有一个主机。每次数据包传输都有一个状态阶段,在该阶段,接收数据的一方可以返回 ACK(确认接收)、NAK(重试)、STALL(错误状态)或不返回任何内容(数据损坏阶段、设备不可用或已断开)。USB 2.0 规格的第 8.5 节对数据包进行了更详细的解释。USB 总线上可以发生四种不同类型的传输:控制传输、批量传输、中断传输和等时传输。以下是这些传输类型及其特性的描述。
主机控制器或主机控制器驱动程序会将大规模传输在 USB 总线上的设备与设备驱动程序之间拆分成多个数据包。
设备请求(控制传输)到默认端点是特殊的。它们由两个或三个阶段组成:SETUP、DATA(可选)和 STATUS。设置数据包被发送到设备。如果有数据阶段,数据包的方向将在设置数据包中给出。状态阶段的方向与数据阶段的方向相反,或者如果没有数据阶段,则为 IN。主机控制器硬件还提供了根端口的当前状态寄存器,并且记录自上次重置状态更改寄存器以来发生的更改。通过虚拟集线器访问这些寄存器,正如 USB 规格中所建议的那样。虚拟集线器必须符合该规格第 11 章中给出的集线器设备类。它必须提供一个默认管道,通过该管道可以发送设备请求。它返回标准的 andhub 类特定描述符集。它还应该提供一个中断管道,报告其端口发生的更改。目前有两种主机控制器的规格可用:英特尔的通用主机控制器接口(UHCI)和康柏、微软和国家半导体的开放主机控制器接口(OHCI)。UHCI 规格旨在通过要求主机控制器驱动程序提供每帧的完整传输调度来减少硬件复杂性。而 OHCI 类型的控制器则更加独立,提供了一个更加抽象的接口,能够自己完成大量工作。
UHCI 主机控制器维护一个帧列表,其中包含 1024 个指向每帧数据结构的指针。它理解两种不同的数据类型:传输描述符(TD)和队列头(QH)。每个 TD 代表一个要与设备端点通信的数据包。QH 是将 TD(和 QH)组合在一起的一种方式。
每个传输由一个或多个数据包组成。UHCI 驱动程序将大规模传输拆分为多个数据包。除了等时传输外,每个传输都会分配一个 QH。对于每种传输类型,这些 QH 会在该类型的 QH 中收集。由于固定的延迟要求,等时传输必须首先执行,并且通过帧列表中的指针直接引用。最后一个等时 TD 会引用该帧的中断传输的 QH。所有中断传输的 QH 都指向控制传输的 QH,而控制传输的 QH 又指向批量传输的 QH。以下图示提供了一个图形化概览:
这导致在每个帧中运行以下调度。在从帧列表中获取当前帧的指针后,控制器首先执行该帧中所有等时数据包的 TD。最后一个等时 TD 会引用该帧的中断传输的 QH。然后主机控制器将从该 QH 继续执行各个中断传输的 QH。在完成该队列后,中断传输的 QH 将引用控制传输的 QH。控制器将执行所有在该队列中安排的子队列,随后执行所有在批量 QH 队列中的传输。为了便于处理已完成或失败的传输,硬件会在每帧结束时生成不同类型的中断。在传输的最后一个 TD 中,主机控制器驱动程序会设置“完成时中断”位,以便在传输完成时触发中断。如果 TD 达到其最大错误计数,则会触发错误中断。如果在 TD 中设置了短包检测位,并且传输的数据包长度小于设定的包长度,则会触发此中断,通知控制器驱动程序传输已完成。主机控制器驱动程序的任务是确定哪个传输已完成或产生错误。当中断服务程序被调用时,它将定位所有已完成的传输并调用其回调函数。
请参考 UHCI 规格以获得更详细的描述。
编程 OHCI 主机控制器要简单得多。控制器假定一组端点是可用的,并且知道调度优先级以及帧内传输类型的顺序。主机控制器使用的主要数据结构是端点描述符(ED),每个 ED 附加一个传输描述符(TD)队列。ED 包含该端点允许的最大数据包大小,控制器硬件负责将传输拆分为多个数据包。数据缓冲区的指针会在每次传输后更新,当起始指针和结束指针相等时,TD 会被退回到完成队列中。四种类型的端点(中断、等时、控制和批量)各自有自己的队列。控制和批量端点在各自的队列中排队。中断 ED 以树状结构排队,树的层级定义了它们运行的频率。
主机控制器在每帧中运行的调度如下。控制器首先运行非周期性的控制和批量队列,直到 HC 驱动程序设置的时间限制为止。然后,使用帧编号的低五位作为索引,运行该帧的中断传输,进入中断 ED 树的第 0 层。树的末端连接了等时 ED,这些 ED 会随后被遍历。等时 TD 包含第一次运行传输的帧编号。所有周期性传输完成后,将再次遍历控制和批量队列。定期调用中断服务程序来处理完成队列,调用每个传输的回调并重新调度中断和等时端点。
请参考 UHCI 规格以获得更详细的描述。中间层提供对设备的受控访问,并维护不同驱动程序和服务层使用的资源。该层负责以下方面:
设备配置的信息
与设备通信的管道
设备的探测、附加和分离
每个设备提供不同级别的配置信息。每个设备有一个或多个配置,其中一个配置在探测/附加过程中被选中。一个配置提供电力和带宽要求。在每个配置中,可以有多个接口。设备接口是端点的集合。例如,USB 扬声器可以有一个用于音频数据的接口(音频类)和一个用于旋钮、拨盘和按钮的接口(HID 类)。配置中的所有接口同时处于活动状态,并可以被不同的驱动程序附加。每个接口可以有不同的备用选项,提供不同的服务质量参数。例如,在摄像头中,这用于提供不同的帧大小和每秒帧数。
在每个接口中,可以指定 0 个或多个端点。端点是与设备进行通信的单向访问点。它们提供缓冲区来暂时存储来自设备的输入或输出数据。每个端点在一个配置中有一个唯一的地址,由端点的编号和方向组成。默认端点,即端点 0,不属于任何接口,并且在所有配置中可用。它由服务层管理,设备驱动程序不能直接访问。
这种层级的配置信息通过一组标准的描述符在设备中进行描述(见 USB 规格的第 9.6 节)。可以通过获取描述符请求来请求这些描述符。服务层缓存这些描述符,以避免在 USB 总线上进行不必要的传输。可以通过函数调用来访问这些描述符。
设备描述符:关于设备的一般信息,如供应商、产品和修订号、支持的设备类、子类和协议(如果适用)、默认端点的最大数据包大小等。
配置描述符:该配置中接口的数量、支持的挂起和恢复功能以及电力要求。
接口描述符:接口类、子类和协议(如果适用)、接口的备用设置数量和端点数量。
端点描述符:端点地址、方向和类型、最大数据包大小支持及其轮询频率(如果类型是中断端点)。默认端点(端点 0)没有描述符,且在接口描述符中永远不计入。
字符串描述符:在其他描述符中,一些字段提供字符串索引。这些索引可以用来获取描述性字符串,可能是多种语言版本。
类规格可以添加自己的描述符类型,这些可以通过获取描述符请求来访问。
与设备端点的通信通过所谓的管道进行。驱动程序将传输提交给端点的管道,并提供一个回调函数,以便在传输完成或失败时调用(异步传输),或者等待传输完成(同步传输)。端点的传输在管道中被串行化。传输可以完成、失败或超时(如果已设置超时)。传输有两种类型的超时。超时可能由于 USB 总线上的超时(毫秒)发生。这些超时被视为失败,可能是由于设备断开连接。第二种超时形式是在软件中实现的,当传输未在指定时间内完成(秒)时触发。这些超时是由设备对传输的包进行否定确认(NAK)引起的。发生这种情况的原因可能是设备尚未准备好接收数据、缓冲区下溢或上溢,或协议错误。
如果通过管道的传输大于关联的端点描述符中指定的最大数据包大小,主机控制器(OHCI)或主机控制器驱动程序(UHCI)将把传输拆分成最大数据包大小的多个数据包,最后一个数据包的大小可能小于最大数据包大小。
有时,设备返回的数据少于请求的数据量并不是问题。例如,向调制解调器进行的批量输入传输可能请求 200 字节的数据,但调制解调器在那个时刻只有 5 字节的数据可用。驱动程序可以设置短数据包(SPD)标志。这允许主机控制器接受一个数据包,即使传输的数据量少于请求的数据量。此标志仅对输入传输有效,因为发送到设备的数据量始终是预先知道的。如果在设备传输过程中发生无法恢复的错误,管道将被暂停。在接受或发送更多数据之前,驱动程序需要解决暂停的原因,并通过发送清除端点暂停设备请求通过默认管道来清除端点的暂停条件。默认端点不应暂停。
有四种不同类型的端点及其对应的管道:
控制管道 / 默认管道:每个设备有一个控制管道,连接到默认端点(端点 0)。此管道承载设备请求和相关数据。通过默认管道与其他管道之间的传输区别在于,传输的协议在 USB 规格中有所描述。这些请求用于重置和配置设备。第 9 章中提供了每个设备必须支持的一组基本命令。设备类规格可以扩展这些命令,以支持更多的功能。
批量管道:这是 USB 等同于原始传输介质的管道。
中断管道:主机向设备发送请求获取数据,如果设备没有数据可发送,则会对数据包进行 NAK(否定确认)。中断传输在创建管道时指定的频率下进行调度。
同步管道:这些管道用于同步数据,例如视频或音频流,具有固定的延迟,但不保证数据的交付。目前的实现中支持这种类型的管道。控制、批量和中断传输中的数据包在传输过程中如果发生错误或设备因缺乏存储接收数据的缓冲区而对数据包进行 NAK 时,会进行重试。然而,若同步数据包未成功交付或数据包被 NAK,系统不会重试,因为这可能违反时间约束。
必要带宽的可用性在创建管道时进行计算。传输在 1 毫秒的时间框架内进行调度。每个框架内的带宽分配由 USB 规格第 5.6 节规定。同步和中断传输可以使用框架带宽的最多 90%。控制和批量传输的数据包在所有同步和中断数据包之后调度,并将消耗剩余的带宽。
有关传输调度和带宽回收的更多信息,请参阅 USB 规格第 5 章、UHCI 规格第 1.3 节和 OHCI 规格第 3.4.2 节。
在集线器通知新设备连接后,服务层会打开端口,给设备提供 100 毫安的电流。此时,设备处于默认状态,并监听设备地址 0。服务层接着会通过默认管道检索各种描述符。之后,它会发送一个设置地址请求,将设备移出默认设备地址(地址 0)。多个设备驱动程序可能能够支持该设备。例如,一个调制解调器驱动程序可能能够通过 AT 兼容接口支持 ISDN TA(终端适配器)。然而,针对特定型号的 ISDN 适配器的驱动程序可能能为该设备提供更好的支持。为了支持这种灵活性,探测程序返回优先级,表示支持的级别。对特定产品版本的支持优先级最高,而通用驱动程序的优先级最低。如果一个配置中有多个接口,多个驱动程序也可能附加到同一个设备。每个驱动程序只需要支持接口的一个子集。
对于新连接设备的驱动程序探测,首先检查是否有设备特定的驱动程序。如果没有找到,探测代码会遍历所有支持的配置,直到有驱动程序在某个配置中附加。为了支持具有多个驱动程序的设备(位于不同的接口上),探测程序会遍历所有尚未被驱动程序占用的接口。超过集线器功率预算的配置会被忽略。在附加过程中,驱动程序应该初始化设备到正确的状态,但不应重置设备,因为这会导致设备断开与总线的连接,并重新启动探测过程。为了避免不必要的带宽消耗,驱动程序不应在附加时就占用中断管道,而应推迟分配该管道,直到文件被打开并且数据实际使用时。当文件关闭时,即使设备仍然附加,管道也应该被关闭。
设备驱动程序应预期在与设备的任何事务中接收到错误。USB 设计支持并鼓励设备在任何时间断开连接。驱动程序应确保在设备消失时执行正确的操作。
此外,已断开并重新连接的设备不会重新附加到相同的设备实例。未来,随着更多设备支持序列号(参见设备描述符)或其他定义设备身份的方法得到开发,这种情况可能会发生变化。
设备的断开连接通过集线器在传递给集线器驱动程序的中断数据包中信号传递。状态更改信息指示哪个端口发生了连接变化。为该端口连接的设备的所有设备驱动程序都会调用设备分离方法,并清理相应的数据结构。如果端口状态指示此时该端口上已连接了一个设备,将启动设备的探测和附加过程。设备重置将产生集线器上的断开-连接序列,按上述方式处理。
USB 规范未定义除默认管道外其他管道上的协议。有关此协议的信息可以通过各种渠道找到。最准确的来源是 USB 官方主页上的开发者部分。从这些页面上,越来越多的设备类规范可以获取。这些规范指定了合规设备从驱动程序角度应该具备的外观、需要提供的基本功能以及通信通道上使用的协议。USB 规范包括了集线器类的描述。为了满足键盘、平板、条形码阅读器、按钮、旋钮、开关等设备的需求,已经创建了人机接口设备(HID)类规范。另一个例子是大容量存储设备的类规范。有关完整的设备类列表,请参见 USB 官方主页上的开发者部分。
然而,对于许多设备,协议信息尚未发布。有关所使用协议的信息可能可以从制造该设备的公司获得。一些公司要求你在提供规范之前签署保密协议(NDA)。在大多数情况下,这意味着无法将驱动程序开源。
另一个有用的信息来源是 Linux 驱动源代码,因为许多公司已经开始为其设备提供 Linux 驱动程序。联系这些驱动程序的作者获取信息源是一个好主意。
示例:人机接口设备
像键盘、鼠标、平板、按钮、旋钮等人机接口设备的规范在其他设备类规范中有所提及,并且被许多设备使用。
例如,音频扬声器提供连接到数字-模拟转换器的端点,可能还会为麦克风提供额外的管道。它们还在一个单独的接口中为设备正面的按钮和旋钮提供 HID 端点。监视器控制类也是如此。通过现有的内核和用户空间库结合 HID 类驱动程序或通用驱动程序,构建这些接口的支持是直接的。另一个示例是内置传统鼠标端口的廉价键盘。为了避免将 USB 集线器硬件包含到设备中,制造商将从键盘后部 PS/2 端口接收到的鼠标数据与键盘按键压下的数据,合并成同一配置中的两个独立接口。鼠标和键盘驱动程序分别附加到相应的接口,并为这两个独立端点分配管道。
示例:固件下载
许多基于通用处理器的设备在其上添加了 USB 核心。由于 USB 设备的驱动程序和固件开发仍然非常新,许多设备在连接后需要下载固件。
其过程是直接的。设备通过供应商和产品 ID 来识别自己。第一个驱动程序探测并附加到设备,并将固件下载到设备中。之后,设备会自我重置,驱动程序被分离。稍作暂停后,设备会在总线上宣布其存在。设备会更改其供应商/产品/修订 ID,以反映其已加载固件的事实,因此第二个驱动程序会探测并附加到该设备。
这类设备的一个例子是基于 EZ-USB 芯片的 ActiveWire I/O 板。该芯片有一个通用固件下载器可用。下载到 ActiveWire 板中的固件会更改修订 ID,然后它将执行 EZ-USB 芯片 USB 部分的软重置,断开与 USB 总线的连接,并重新连接。
示例:大容量存储设备
对大容量存储设备的支持主要围绕现有协议构建。Iomega 的 USB Zipdrive 基于其驱动的 SCSI 版本。SCSI 命令和状态消息被封装在块中,通过 bulk 管道进行传输,从设备到主机和从主机到设备,模拟通过 USB 线的 SCSI 控制器。ATAPI 和 UFI 命令也以类似的方式得到支持。
大容量存储规范支持两种不同的命令块封装方式。最初的尝试是通过默认管道发送命令和状态,并使用 bulk 传输将数据在主机和设备之间传送。根据经验,设计了第二种方法,它基于封装命令和状态块,并通过 bulk out 和 in 端点发送。规范明确规定了必须在何时发生什么,以及在遇到错误条件时应该做什么。编写这些设备驱动程序时最大的问题是将基于 USB 的协议适配到现有的大容量存储设备支持中。CAM 提供了钩子,能够以相对直接的方式完成此操作。ATAPI 则更复杂,因为历史上 IDE 接口并未有很多不同的表现形式。
Y-E Data 的 USB 软盘支持则更不简单,因为设计了一个新的命令集。