首页 > 范文大全 > 正文

USB设备枚举过程及代码设计

开篇:润墨网以专业的文秘视角,为您筛选了一篇USB设备枚举过程及代码设计范文,如需获取更多写作素材,在线客服老师一对一协助。欢迎您的阅读与分享!

摘要:usb枚举程序是USB设备固件程序的基础和核心,主要针对USB设备驱动的这一核心进行了研究。使用LeCroyUSB总线分析仪捕获了USB接口扫描枪总线处理过程图,通过解析此图从而详细分析了主机对USB设备枚举的全过程。并针对主机在枚举过程中每个不同请求,主要设计了设备如何响应这些请求的枚举代码框架,即枚举类,包括Set_Address 、Get_Descriptor、Get_Status、Set_Configuration等设备响应主机请求时进行的操作。与此同时,也分析了一些枚举出错的案例及可用解决方法。

关键词:USB;枚举;枚举代码;枚举出错;总线处理

中图分类号:TP334文献标识码:A文章编号:1009-3044(2010)19-5269-04

The Process and Code Design for USB Enumeration

QIAN Hao

(Department of Software Engineering, Tongji University, Shanghai 201804, China)

Abstract: In a USB driver program, enumeration is the basic and most important part. For this problem, with the help of the picture caught by LeCroy which can catch the bus process for USB, a detailed analysis of the host’s enumeration process to USB devices has been given. And for the host’s different requests in the enumeration, an enumeration class including a lot of methods for USB devices is designed to correspond to the host. For example, the methods of set_address 、get_descriptor、get_status and set_configuration are presented. Also some enumeration error cases with their way to be resolved are presented.

Key words: USB; enumeration; enumeration class; enumeration error; enumeration process

USB 枚举就是从带有 USB接口的设备读取一些信息,如设备类型、通信类型等,这样主机就可以通过这些信息加载合适的驱动程序,加载成功后, 主机才能和设备进行正常通信。

1 基本概念

主机使用轮讯总线方式实现枚举,主机控制端口提出相应的请求,设备作出相应的处理,这就是主机和设备之间的枚举过程,这也是一个命令(请求)的过程。 USB定义了一些命令,所有USB设备在设备缺省控制通道(EP0)处对主机命令发出响应。这些命令是通过使用控制传输来达到的,命令及命令的参数通过Setup包发向设备,由主机负责设置Setup包(8个字节)内的每个域的值。所有的USB设备都要对主机发给自己的命令作出响应,规范定义了11个标准命令:Clear_Feature、Get_Configuration、Get_Descriptor、Get_Interface、Get_Status、Set_Address、Set_Configuration、Set_Descriptor、Set_Interface、Set_Feature、Synch_Frame。

下面将使用LeCory捕捉一个带有USB接口的扫描枪设备(如超市使用的条码扫描枪)的总线处理图,对其枚举过程进行分析,同时给出其枚举代码。

1.1 类定义

1.1.1 枚举代码类

class UsbEnum : public UsbDebug

{

public:

UsbEnum(): UsbDebug() {};

virtual ~UsbEnum();

protected:

virtual void stall_ep0(void);//返回一个STALL握手包

virtual void setup_handler(void);

void data_transmit(UCHAR *pRomData, USHORT len); //设备返回数据包接口

void get_status(void);

void set_address(void);

void get_descriptor(void);

void set_configuration(void);

};

假设上层类UsbDebug已经完成了主机请求数据包的捕获,并放在Data_ PacketSetupData结构体中,而枚举信息则放入其包含的DEVICE_REQUEST DeviceRequest结构体变量中,这个结构提变量包括bmRequestType(特定请求的特征)、bRequest(特定请求)、wValue(传递参数给设备)、wIndex(指定接口或端点)、wLength(控制传输的数据阶段中传输数据的字节大小)。

1.1.2 特定请求响应函数

inline void UsbEnum::setup_handler(void)

{

UCHAR type, req;

type = (SetupData.DeviceRequest.bmRequestType) & 0x60;

req = SetupData.DeviceRequest.bRequest & 0x0f ;

if (type == USB_STANDARD_REQUEST)

{

switch( req )

{

case 0x00:

get_status();

break;

case 0x01:

clear_feature();

break;

……. //其它请求省略

default:

stall_ep0();

break;

}

}

else

stall_ep0();

}

Type为bmRequestType 的bit[6:5], 它表示了请求所属的类型,USB标准中定义了所有USB设备必须支持的标准请求,此外,群组和供应商还定义了一些其他的请求,函数只对标准请求进行处理,其它情况均返回STALL握手包,而不是ACK或者NAK包,表示没有能够识别该请求。Req表示执行get_status()、clear_feature()等中的哪个请求,从而进行处理。

1.2 过程分析与代码设计

图1是LeCroy USB总线分析仪捕获的USB总线处理过程,通过此图可以来帮助理解设备枚举代码是如何工作的。

1.2.1 插入设备并初始化

当主机用轮询方式检测到USB端口有新设备时,主机就会给HUB发送总线复位命令,要求复位,设备完成初始化。主机根据D+与D-之间的电压差识别是否有新设备接入,并根据是D+还是D-被拉高来判断到底是什么设备(全速/低速)。主机使用缺省的CONTROL端点EP0 (图1“ENDP”)向设备发送请求,最初向默认地址0 (图1“ADDR”)发送请求。

1.2.2 复位

主机先发送Get_Descriptor-Device请求(图1Transfer 0),以确定设备EP0缓冲区的数据包最大长度(64字节)。然后主机发送总线复位(数据包76),对设备进行复位。这样便设置了USB总线复位IRQ和其寄存器位。而设备根据主机请求通过特定请求响应函数setup_handler选择进行相应类型的响应处理。

1.2.3 Set_Address

在Transfer 1中,主机使用Set_Address请求,给外设分配特定地址,将分配地址装入功能地址寄存器。以后设备只响应指向地址1的请求。在主机进行总线复位或者设备断开之前,这一地址始终保持有效。注意,Transfer 1之后,总线处理过程中的ADDR由0变成了1。

inline void UsbEnum::set_address(void) {

enum_BusState = USB_ADDR_ASSIGNED;// 返回addressed state

StatusAckStat(1); 设置ACKSTAT位终止该请求 }

如上代码,这是所有请求中最简单的请求,程序需要做的所有工作就是设置ACKSTAT位来终止请求。USB控制器会对其进行处理,它以新的地址自动更新内部地址寄存器,以后设备仅仅响应指向该地址的请求。如图2,它并没有像Get_Descriptor操作一样有个OUT操作。

1.2.4 Get_Descriptor

在Transfer 2至13中,主机请求各个描述符。设备需要从8个设置字节中确定发送哪个描述符,使用该信息来访问描述符数组之一,将这些字符装入端点FIFO,准备回送给主机。

图3展开了图1中的Transfer 7,展示了数据包层次的处理过程。

inline void UsbEnum::get_descriptor(void)

{

UCHAR bDescriptorType = (((SetupData.DeviceRequest.wValue) >> 8) & 0xFF); //bDescriptorType为描述符的类型,由wValue域高字节决定

switch(bDescriptorType)

{

Case 0x01: //设备描述符

{

if (SetupData.DeviceRequest.wLength > sizeof(USB_DEVICE_DESCRIPTOR))

SetupData.DeviceRequest.wLength = sizeof(USB_DEVICE_DESCRIPTOR);

data_transmit((UCHAR *)DeviceDescr, SetupData.DeviceRequest.wLength);

break;

}

case 0x02: …

case 0x03: //0x03 字符串描述符

switch( (SetupData.DeviceRequest.wValue) & 0xff )// 字符串描述符类型由wValue域低字节决定

{根据不同类型进行不同的操作;}

break;

case ….

default:

stall_ep0();

break; }

}

整段代码从取得描述符类型开始,后通过Switch语句选择相应操作。在此,主机要求64字节,但是程序仅回传了18字节。到底怎么回事? 其实以上处理过程是很正常的USB事件,这是由一个简单的规则决定的,总是在(a)主机请求的字节数,以及(b)实际具有的字节数之间取较小的字节数目回送。一个设备描述符包括18个字节,而主机要求64个字节,因此固件正确地回送了18个字节,这在代码中也有所体现。而数据包的返回由data_transmit函数完成。

1.2.5 Get_Status

主机给设备挂载驱动,主机通过解析描述符后对设备有了足够的了解,会选择一个最合适的驱动给设备。设备是通过get_status函数返回主机请求的描述符数据包。

inline void UsbEnum::get_status(void)

{

UCHAR bRecipient = SetupData.DeviceRequest.bmRequestType & USB_RECIPIENT;

if(ControlData.DeviceRequest.wValue == 0 && ControlData.DeviceRequest.wLength == 2 ){

switch(bRecipient)

{

case USB_RECIPIENT_DEVICE:

data_transmit(…);

break;

case USB_RECIPIENT_INTERFACE:…

case USB_RECIPIENT_ENDPOINT:

default:

stall_ep0();

break;

}

}

else

stall_ep0();

}

需要返回的数据类型由bmRequestType字段的bit[4:0] 决定指定。如代码,通过bit[4:0],决定返回设备、接口或端点之一的状态。而wValue 和 wLength必须为0和2,不然请求无效。

如果该设备以前在系统上成功枚举过,操作系统会根据以前记录的登记信息而非inf文件挂载驱动。当操作系统给设备指定了驱动之后,就由驱动来负责对设备的访问。

1.2.6 Set_Configuration

设备驱动选择一个配置,驱动根据前面设备回复的信息,发送Set_Configuration请求来正式确定选择设备的哪个配置作为工作配置,设备通过如下的set_configuration代码实现。

Inline

void UsbEnum::set_configuration(void)

{

if (SetupData.DeviceRequest.wValue == 0)

{ bflags.bits.configuration = 0;

m_BusState=USB_ADDR_ASSIGNED;

}

else if (ControlData.DeviceRequest.wValue == 1)

{

bflags.bits.configuration = 1;

….

m_BusState = USB_CONFIGURED;

}

} else

stall_ep0();

}

该请求中的wValue域的低字节表示设置的值,如果设置值等于0,表示设备进入地址状态,如果wValue为1值,那么表示设备进入配置状态,进行相应得配置。

至此,设备处于配置状态, 配置完成后设备可使用。

2 枚举出错问题解决

2.1 上电前设备枚举

扫描枪在PC启动前就通过USB接口连接在PC上,然后启动PC机,这样可能会枚举不成功,其实原因很简单,就是主机发送GetDescriptor请求给设备的时候设备还没上电,上电前插入的设备在GetDescriptor的时候会失败,失败后usb驱动会执行再继续进行尝试,在失败3次后端口失效,造成无法识别设备,这样的情况可以在驱动代码加个延时,延迟GetDescriptor的发送时间,这当然要根据每种类型的主板给USB的上电时间。

2.2 设备的特殊设置

在set_configuration时加入特殊操作时也会出现问题,譬如USB模拟RS232,并且模拟RTS电平,要求在插入设备时设备RTS拉高,即PC端CTS电平拉高,这主要适用于一些特殊要求,譬如说工厂需要根据CTS(设备RTS)信号是否为高来判断此设备是否安全连接在PC 机上,拉高RTS可以在set_configuration进行操作,但必须在get_status执行之后,所以可以加个全局bool变量,在get_status成功后使其为true,然后在set_configuration中以这个全局变量的值作为RTS拉高这个操作的条件;如果不在get_status之后执行配置会导致枚举的失败。

3 结束语

分析了主机对USB设备枚举的全过程,并以C++类定义了USB设备枚举时所有响应主机请求的操作,可以作为USB驱动中枚举过程的一个通用接口。

参考文献:

[1] 陈启美,吴永辉,丁传锁,等.USB主机――硬件及软件[J].电力自动化设备,2001,21(6):55-58.

[2] 陈启美,吴永辉,丁传锁,等.USB协议层[J].电力自动化设备,2001,21(5):59-63.

[3] 程斓,杨子杰.基于PDIUSBD12的USB设备固件程序开发[J].计算机应用,2001,24(7):150-152.

[4] 张希英,樊光辉,李传珍.USB通信技术[J].北京广播学院学报,2004,11(4):55-64.

[5] 唐劲飞,可永敏,穆连运,等.基于Windows的USB设备开发[J].舰船电子工程,2009,29(11):140-142.

[6] 张海峰,赵爱玲.USB总线的初始化分析[J].计算机网络与通信,2007,29(2):32-34.

[7] 赵建领,薛园园.USB应用开发实例详解[M].北京:人民邮电出版社,2009.

[8] 王宜怀,刘晓升.嵌入式技术基础与实践[M].北京:清华大学出版社,2007.