通过前面的学习,大家应该对USB固件程序结构有了比较深的认识,现在再来详细说说固件里决定设备识别成厂商自定义USB设备的地方有哪些,或者说厂商自定义USB设备的固件特性有哪些。
之前不止一次说过学习USB固件编程的要点是要学会各种描述符及主机命令以及设备的枚举过程,理解设备枚举过程是主线,在这一过程中需要掌握主机各种命令及USB相关描述符(本站《USB命令请求及描述符详解(速查手册)》和《用USB Monitor监视USB枚举(配置、识别)过程(USB枚举过程分析)》对此进行了总结),一般USB接口芯片生产商都提供有固件范例,但USB设备类型非常多,不可能都有相应的示例,我们设计固件时可以直接在范例的基础上修改,修改的地方也主要是各种描述符以及命令的处理上(有些设备类甚至都不需要修改命令处理代码)。影响设备是否被识别成厂商自定义USB设备固件程序的地方主要在以下几个地方:
1、设备描述符
- bDeviceClass 字段,设备类码,此段为0xFF时,设备的类型由厂商定义
- bDeviceSubClass字段,设备子类掩码,如果bDeviceClass字段的值为0xFF,此字段的值也必须为0xFF
- bDevicePortocol字段,设备协议码,如果此字段的值为0xFF,此设备使用厂商定义的协议
修改好的设备描述符:
//设备描述符 code USB_DEVICE_DESCRIPTOR DeviceDescr = { sizeof(USB_DEVICE_DESCRIPTOR), //设备描述符长度,= 12H USB_DEVICE_DESCRIPTOR_TYPE, //设备描述符类型,= 01H 0x00,0x01, //协议版本,= 1.10 0xFF, //测试设备类型 ,=0xFF为厂商自定义设备 0xFF, //设备协议 EP0_PACKET_SIZE, //端点0最大数据包大小,= 10H 0x71,0x04, //PHILIPS公司的设备ID 0x00,0x09, //设备制造商定的产品ID 0x00,0x01, //设备系列号 , , , //索引 //可能的配置数 };
2、接口描述符
- bInterfaceClass字段,接口所属于类别,为0xFF时代表厂商自定义
- bInterfaceSubClass字段,接口子类码,只说明了当bInterfaceClass为零时此字段也必须为零,这里暂时也设为0xFF
bInterfaceProtocol字段,接口协议,对于厂商自定义设备并没有说明,这时暂时设为0xFF
修改好的接口描述符:
//接口描述符 { sizeof(USB_INTERFACE_DESCRIPTOR), //接口描述符长度,= 09H USB_INTERFACE_DESCRIPTOR_TYPE, //接口描述符类型,= 04H , //接口数,只有1个 , //可选配置,只有1个 NUM_ENDPOINTS, //除端点0的端点索引数目,= 04H 0xFF, //接口别类,0xFF=厂商自定义 0xFF, //子类代码 0xFF, //协议代码 //字符串描述符索引 },
3、对USB主机命令的处理
在《USB命令请求及描述符详解(速查手册)》一文的表1列出了USB主机命令结构,其中bmRequestType字段的第5至第6位(D5~D6)命令种类(标准、类或者厂商自定义),当为厂商自定义命令时,此时bRequest字段的值就是厂商自定义的命令ID,wValue字段可以传送两个字节长度的数据,当然厂商自定义的命令也可能没有带数据,或者带的数据不止两个字节,如果带的数据不止两个字节,那么应该在wLength字段指明所带数据长度,而真正的数据将在紧接着的数据阶段传输。
厂商自定义命令有哪些?那当然是按设备需要具备的功能,由厂商自己来定义了,比如此开源项目,我打算实现的自定义USB设备可以通过主机来控制电路板上的八个LED,还可以读取板子上的按键状态,这两个功能都可以用厂商定义命令来实现。比如可以定义USB命令的bRequest字段为0时代表控制LED状态,为1时读取按键状态。控制LED状态时肯定主机要带数据来表明LED应该转换的状态,我们可以用一个字节的八个位分别对应八个LED的状态,那么所带数据就可以放在wValue字段里。而读取按键状态需要两步,第一步是主机发自定义USB命令,命令ID(即bRequest字段的值)为1,当设备接收到这个命令后,应该立即将当前按键状态通过控制端点发送给主机,按键状态可以用一个字节的低两位表示,主机发送厂商自定义命令是通过Windows API函数DeviceIoControl来完成的,其功能类似于另一个API函数WriteFile,DeviceIoControl发送命令时,如果设备有数据返回,DeviceIoControl函数的第5个参数会带回设备返回的数据,所以在读取按键状态时,调用DeviceIoControl函数后可以查询DeviceIoControl函数里的第5个参数得知按键状态。
下面是需要修改的命令处理相关代码:
1)Chap_9.c文件中的control_handler函数是判断命令别类(即判断是标准、类或者厂商自定义)的,需要增加厂商自定义命令调用接口,可以参考标准请求代码的写法,内容如下:
/************************************************************* ** 函数名称: void control_handler(void) ** 功能描述: 主机命令类型服务程序 **************************************************************/ void control_handler(void) { INT8U type, req,wValue; type = ControlData.DeviceRequest.bmRequestType & USB_REQUEST_TYPE_MASK; //读取请求代码 req = ControlData.DeviceRequest.bRequest & USB_REQUEST_MASK; wValue = ControlData.DeviceRequest.wValue; if (type == USB_STANDARD_REQUEST) (*StandardDeviceRequest[req])(); //标准请求处理 else if (type == USB_VENDOR_REQUEST) //厂商请求 (*VendorDeviceRequest[req])(wValue); //else if(type == USB_CLASS_REQUEST) // (*ClassDeviceRequest[req])(); //类请求,如大容量类 else stall_ep0(); //无效请求,返回STALL }
以上代码中,当判断到USB命令数据包里的bRequest字节为USB_VENDOR_REQUEST(0x02)时调用(*VendorDeviceRequest[req])(wValue) 一句,VendorDeviceRequest是在代码区定义的一个函数指定组数,这种调用方法可以根据数组下标(req)来决定调用哪个函数,VendorDeviceRequest函数指针数组的定义如下(同样是参考标准请求代码的写法):
//USB厂商请求入口地址指针表 code void (*VendorDeviceRequest[]) (INT16U wValue) = { control_led, get_key_state };
当req的值(即命令ID)为0时调用control_led函数,当req的值为1时调用get_key_state函数,同时把wValue作为参数传递给这两个函数(当然参数对get_key_state函数没用),以下是这两个函数的代码:
sbit K1 = P3^; sbit K2 = P3^; /**************************************** ** 函数名称: void control_led(INT16U wValue) ** 功能描述: 控制扩展板EXT-BOARD-A上的LED ** 参 数:INT16U wValue->控制值,低字节有效,如wValue为0x0000时LED全灭,wValue为0x00FF时LED全亮 *****************************************/ void control_led(INT16U wValue) { P0 = wValue % ; //返回一个空的数据表示执行完毕 single_transmit(, ); //返回一个空的数据表示执行完毕 } /**************************************** ** 函数名称: void get_key_state(INT16U wValue) ** 功能描述: 取得扩展板EXT-BOARD-A上按键状态 ** 参 数:INT16U wValue->无意义 *****************************************/ void get_key_state(INT16U wValue) { INT8U ucKeyState[],i; ucKeyState[] = 0x00; K1 = ; K2 = ; ;i<;i++); if(~K1) //K1按下 { ucKeyState[] |= 0x01; } if(~K2) //K2按下 { ucKeyState[] |= 0x02; } single_transmit(ucKeyState,); }
另外还需要在Chap_9.h中增加以下内容,对以上新加函数作申明:
extern code void (*VendorDeviceRequest[])(INT16U wValue); extern void control_led(INT16U wValue); extern void get_key_state(INT16U wValue);
至此,厂商自定义USB设备的代码修改工作已经完成,除了以上说的USB设备固件里影响厂商自定义USB设备特性的地方外,如果设备还用到了非控制端点外的其它端点,还应该定义相应端点描述符,以及用这些端对接收到的数据处理代码以及通过这些端点发送数据的代码。
厂商自定义的命令必须通过控制管道传输,自定义USB设备也可以通过非控制管道传输数据,所以我们也可能过非控制管道的数据来控制LED状态和读取按键状态,只不过需要自己来定义数据包里各个字节的含义,这种用法有点像传统串口通信一样需要定义数据帧格式,比如可以把数据包的第一个字节固定为命令字节,后面是数据。
厂商自定义设备必须有相应的主机驱动程序对设备进行支持,所以光完成上面的工作还不行,还得写相应的主机设备驱动程序才行,除了主机驱动程序,也许还需要相应的主机应用程序,下面的文章我将会一步一步介绍怎么来实现USB设备的Windows驱动程序以及Windows应用程序。
特别备注:当设备被指定为一个USB协议定义里的标准设备类别而主机操作系统没有提供相关驱动程序,这时操作系统也会提示安装驱动程序,如果您所写的驱动程序并没有按USB协议定义的标准设备规范来,那么其实这个设备也等同于一个厂商自定义设备,本开源项目的最初版本定义设备类别是一种测试设备类(bDeviceClass = 0xDC),但写的驱动程序程序却并不符合特定设备类的特性,所以也相当于是一种自定义USB设备,以前发布的EASY USB 51 PROGRMAER第一个版本的时候就是按这种方法做的厂商自定义USB设备,最新PLUS版本对这种不严格的方法进行了更正。
本节完成后的USB固件源码 |