笔者手头有一个 USB温度计,使用的是USB HID协议,无需安装驱动程序即可在不同版本的Windows中直接使用。同时厂家还提供了一个应的程序可以直接在电脑上获得当前环境温度,非常方便。本文介绍如何使用Arduino Uno + USB Host Shield来读取这个USB温度计的返回值。 首先,需要在 Arduino 的IDE 中安装 USBHost Shield 2.0的库,可以从 Library Manager 中轻松的找到并且安装之。 然后,将 USB Host Shield 插在Uno 板子上,并连接好USB温度计。 对于USB设备来说,最重要莫过于描述符(Descriptor)。它反应了整个设备对外的接口状态,因此,首先要做的是:弄清楚这个设备的全部接口。 前述安装的 USB Host库中自带了一些实例,先运行例子中的 USB_Desc 文件。它的作用是读取当前USB 设备的设备描述符(DeviceDescriptor)。运行结果如下: Start 01 -- Device descriptor: Descriptor Length: 12 Descriptor type: 01 USB version: 0200 Device class: 00 Device Subclass: 00 Device Protocol: 00 Max.packet size: 08 Vendor ID: 0C45 Product ID: 7401 Revision ID: 0001 Mfg.string index: 01 Prod.string index: 02 Serial number index: 00 Number of conf.: 01 Configuration descriptor: Total length: 003B Num.intf: 02 Conf.value: 01 Conf.string: 00 Attr.: A0 Max.pwr: 32 Interface descriptor: Intf.number: 00 Alt.: 00 Endpoints: 01 Intf. Class: 03 Intf. Subclass: 01 Intf. Protocol: 01 Intf.string: 00 Unknown descriptor: Length: 09 Type: 21 Contents: 100100012241000705 Endpoint descriptor: Endpoint address: 81 Attr.: 03 Max.pkt size: 0008 Polling interval: 0A Interface descriptor: Intf.number: 01 Alt.: 00 Endpoints: 01 Intf. Class: 03 Intf. Subclass: 01 Intf. Protocol: 02 Intf.string: 00 Unknown descriptor: Length: 09 Type: 21 Contents: 100100012229000705 Endpoint descriptor: Endpoint address: 82 Attr.: 03 Max.pkt size: 0008 Polling interval: 0A Addr:1(0.0.1) 对照 USB 协议,我们来解读每一项的含义【参考1】。 1. 设备描述符 (DeviceDescriptor ) USB 设备描述符(Device Descriptor) | 偏移量 | 域 | 大小 | 值 | 描述 | 0 | bLength | 1 | 0x12 | 本表的字节数 | 1 | bDecriptorType | 1 | 0x01 | 0x01,即设备描述符(Device Descriptor) | 2 | bcdUSB | 2 | 0x0200 | 此描述符兼容USB 2.0设备 | 4 | bDeviceClass | 1 | 0x00 | 设备类码为0表示一个设置下每个接口独立管理类,各个接口各自独立工作。 | 5 | bDeviceSubClass | 1 | 0x00 | 子类码,同上,接口独立,所以这里为0x00 | 6 | bDevicePortocol | 1 | 0x00 | 协议码 ,同上,接口独立,所以这里为0x00 | 7 | bMaxPacketSize0 | 1 | 0x08 | 端点0的最大包大小为8字节 | 8 | idVendor | 2 | 0x0C45 | 厂商标志(USB 组织分配给厂商的固定编号) | 10 | idProduct | 2 | 0x7401 | 产品标志(厂商自定义编号) | 12 | bcdDevice | 2 | 0x001 | 设备版本号 | 14 | iManufacturer | 1 | 0x01 | 描述厂商信息的字符串描述符的索引值。 | 15 | iProduct | 1 | 0x02 | 描述产品信息的字串描述符的索引值。 | 16 | iSerialNumber | 1 | 0x00 | 描述设备序列号信息的字串描述符的索引值。这里为0表示没有这个字符串。 | 17 | bNumConfigurations | 1 | 0x01 | 配置描述符(Configuration Descriptor)的数量 |
2. 接下来是配置描述符(Configuration Descriptor) USB配置描述符(Configuration Descriptor) | 偏移量 | 域 | 大小 | 值 | 描述 | 0 | bLength | 1 | XX | 配置描述符的长度(代码中没有输出) | 1 | bDescriptorType | 1 | XX | 代码中没有输出,实际应该是0x02即配置描述符 | 2 | wTotalLength | 2 | 0x003B | 配置信息的总长(包括配置,接口,端点和设备类及厂商定义的描述符) | 4 | bNumInterfaces | 1 | 0x02 | 此配置所支持的接口个数,这里表示一共有2个接口(接口0,接口1) | 5 | bCongfigurationValue | 1 | 0x01 | 在SetConfiguration()请求中用作参数来选定此配置。 | 6 | iConfiguration | 1 | 0x00 | 描述此配置的字串描述符索引,0x00表示不存在 | 7 | bmAttributes | 1 | 0xA0 | 0xA0 == 0b1010 0000 配置特性:
D7: 保留(设为一)
D5: 远程唤醒
D4..0:保留(设为一)
| 8 | MaxPower | 1 | 0x32 | 在此配置下的总线电源耗费量。以 2mA 为一个单位。0x32=50D 就是100mA。 |
3. 前面已经声明了,本设备有2个接口(Interface),所以下面有2个接口描述符的表格 3.1 接口描述符0 USB接口描述符(Interface Descriptor) | 偏移量 | 域 | 大小 | 值 | 说明 | 0 | bLength | 1 | XX | 此表的字节数(代码中没有输出) | 1 | bDescriptorType | 1 | XX | 接口描述表类(代码中没有输出,此处应为0x04) | 2 | bInterfaceNumber | 1 | 0x00 | 接口号,当前配置支持的接口数组索引(从零开始)。 | 3 | bAlternateSetting | 1 | 0x00 | 可选设置的索引值。 | 4 | bNumEndpoints | 1 | 0x01 | 此接口用的端点数量,如果是零则说明此接口只用缺省控制管道。 | 5 | bInterfaceClass | 1 | 0x03 | 接口所属的类值,0x03表示人机接口类(HID)【参考2】 | 6 | bInterfaceSubClass | 1 | 0X01 | 子类码 【参考2】 | 7 | bInterfaceProtocol | 1 | 0X01 | 协议码【参考2】 | 8 | iInterface | 1 | 0x00 | 描述此接口的字串描述表的索引值。 |
USB端点描述符(EndPoint Descriptor) | 偏移量 | 域 | 大小 | 值 | 说明 | 0 | bLength | 1 | XX | 此描述表的字节数长度(代码中没有输出) | 1 | bDescriptorType | 1 | XX | 端点描述表类(代码中没有输出,此处应为0x05) | 2 | bEndpointAddress | 1 | 0x81 | 此描述表所描述的端点的地址、方向:
端点号为1 , 是输入端点(设备到主机) | 3 | bmAttributes | 1 | 0x03 | 此域的值描述的是在bConfigurationValue域所指的配置下端点的特性。
11=中断传送 | 4 | wMaxPacketSize | 2 | 0x008 | 当前配置下此端点能够接收或发送的最大数据包的大小为8字节 | 6 | bInterval | 1 | 0x0A | 周期数据传输端点的时间间隙为10ms
|
随后的数据是一个HID Descriptor 很明显,这个接口是一个键盘设备。这个USB温度计有一个特别的功能:当用户在其他USB键盘上长按Scroll Lock之后,可以连续输出温度的数据。从上述接口来看,实现的方法是将自身模拟为键盘,激活功能后,使用模拟按键的方式直接输入温度。 3.2 接口描述符1 4. 表10、USB接口描述符的结构 | 偏移量 | 域 | 大小 | 值 | 说明 | 0 | bLength | 1 | XX | 此表的字节数 | 1 | bDescriptorType | 1 | XX | 接口描述表类(此处应为0x04) | 2 | bInterfaceNumber | 1 | 0x01 | 接口号,当前配置支持的接口数组索引(从零开始)。 | 3 | bAlternateSetting | 1 | 0x00 | 可选设置的索引值。 | 4 | bNumEndpoints | 1 | 0x01 | 此接口用的端点数量,如果是零则说明此接口只用缺省控制管道。 | 5 | bInterfaceClass | 1 | 0x03 | 接口所属的类值,0x03表示人机接口类(HID)【参考2】 | 6 | bInterfaceSubClass | 1 | 0x01 | 子类码 | 7 | bInterfaceProtocol | 1 | 0x02 | 协议码: | 8 | iInterface | 1 | 0x00 | 描述此接口的字串描述表的索引值。 |
表12、USB端点描述符的结构 | 偏移量 | 域 | 大小 | 值 | 说明 | 0 | bLength | 1 | XX | 此描述表的字节数长度 | 1 | bDescriptorType | 1 | XX | 端点描述表类(此处应为0x05) | 2 | bEndpointAddress | 1 | 0x82 | 此描述表所描述的端点的地址、方向:
端点号为2
Bit 7: 方向,如果控制端点则略。
1:输入端点(设备到主机) | 3 | bmAttributes | 1 | 0x03 | 此域的值描述的是在bConfigurationValue域所指的配置下端点的特性。
11=中断传送
| 4 | wMaxPacketSize | 2 | 0x008 | 当前配置下此端点能够接收或发送的最大数据包的大小为8字节 | 6 | bInterval | 1 | 0x0A | 周期数据传输端点的时间间隙为10ms
|
同样的,在USB Host Shield 2.0的库中还有一个HID描述符分析的代码USBHID_desc.c。可以用来进行简单的分析。从前面的结果得知,这个设备有2个HIDInterface,第一个是键盘设备,所以只要关注第二个设备即可。对应的USBHID_desc.ino中我们需要修改两处GetReportDescr函数的参数,让它取得第二个Interface的信息。 uint8_t HIDUniversal2::OnInitSuccessful() { uint8_t rcode; HexDumper<USBReadParser, uint16_t, uint16_t> Hex; ReportDescParser Rpt; if ((rcode =GetReportDescr(1, &Hex))) gotoFailGetReportDescr1; if ((rcode =GetReportDescr(1, &Rpt))) gotoFailGetReportDescr2; return 0; FailGetReportDescr1: USBTRACE("GetReportDescr1:"); goto Fail; FailGetReportDescr2: USBTRACE("GetReportDescr2:"); goto Fail; Fail: Serial.println(rcode, HEX); Release(); return rcode; } 运行结果如下。 上面获得结果中给出了HID Descriptor: 0000: 06 00 FF 09 01 A1 01 09 01 15 00 26 FF 00 75 08 0010: 95 08 81 02 09 01 95 08 91 02 05 0C 09 00 15 80 0020: 25 7F 75 08 95 08 B1 02 C0 对于枯燥的表格,可以使用来自【参考3】的工具进行分析,结果如下: 0x06, 0x00, 0xFF, //Usage Page (用户自定义格式) 0x09, 0x01, //Usage (0x01) 0xA1, 0x01, //Collection (Application) 0x09, 0x01, // Usage (0x01) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x75, 0x08, // Report Size (8) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data,Var,Abs,NoWrap,Linear,Preferred State,No Null Position) 0x09, 0x01, // Usage (0x01) 0x95, 0x08, // Report Count (8) 0x91, 0x02, // Output (Data,Var,Abs,NoWrap,Linear,Preferred State,No Null Position,Non-volatile) 0x05, 0x0C, // Usage Page (Consumer) 0x09, 0x00, // Usage (Unassigned) 0x15, 0x80, // Logical Minimum (128) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x08, // Report Count (8) 0xB1, 0x02, // Feature (Data,Var,Abs,NoWrap,Linear,Preferred State,No Null Position,Non-volatile) 0xC0, // End Collection 就是说数据是通过8个字节的数组来进行传输的。接下来需要仔细研究USB温度计二次开发文档了,在给出的资料中,提到需要使用特定的命令来读取温度: bCommandReadTemper:array[0..8] of Byte=( 0, 1, $80, $33, 1, 0, 0, 0, 0 ); 结合USB逻辑分析仪抓包的结果,可以看到使用了 SET_REPORT Package将command直接发送给了设备。 就是说,要先把这个bCommandReadTemper 命令发送给USB 温度计,然后温度计才能做出响应,送回当前温度值。因此,还要修改库文件中的 hiduniversal.cpp,手工构造这一个过程,同时注意温度计在第二个 Interface 上: uint8_t HIDUniversal: oll() { uint8_t rcode= 0; if(!bPollEnable) return0; if((long)(millis() - qNextPollTime) >= 0L) { qNextPollTime = millis() + pollInterval; uint8_t buf[constBuffLen]; //LABZ_Start // bmRequest = Host todevice (0x00) | Class (0x20) | Interface (0x01) = 0x21, bRequest = Set Report(0x09), Report ID (0xF5), Report Type (Feature 0x03), interface (0x00),datalength, datalength, data buf[0]=0x01; buf[1]=0x80; buf[2]=0x33; buf[3]=0x01; buf[4]=0x00; buf[5]=0x00; buf[6]=0x00; buf[7]=0x00; pUsb->ctrlReq(bAddress,0, bmREQ_HID_OUT, HID_REQUEST_SET_REPORT, 0x00, 0x02, 0x01, 8, 8, buf, NULL); //LABZ_End //LABZfor(uint8_t i = 0; i < bNumIface; i++) { for(uint8_t i = 1; i <bNumIface; i++) { //LABZ uint8_t index = hidInterfaces.epIndex[epInterruptInIndex]; uint16_t read = (uint16_t)epInfo[index].maxPktSize; ZeroMemory(constBuffLen, buf); uint8_t rcode = pUsb->inTransfer(bAddress, epInfo[index].epAddr,&read, buf); if(rcode) { if(rcode !=hrNAK) USBTRACE3("(hiduniversal.h) Poll:", rcode, 0x81); return rcode; } if(read > constBuffLen) read =constBuffLen; bool identical = BuffersIdentical(read, buf, prevBuf); SaveBuffer(read, buf, prevBuf); if(identical) return 0; #if 0 Notify(PSTR("\r\nBuf: "), 0x80); for(uint8_t i = 0; i < read; i++) { D_PrintHex<uint8_t> (buf, 0x80); Notify(PSTR(" "), 0x80); } Notify(PSTR("\r\n"), 0x80); #endif ParseHIDData(this, bHasReportId, (uint8_t)read, buf); HIDReportParser *prs =GetReportParser(((bHasReportId) ? *buf : 0)); if(prs) prs-> arse(this, bHasReportId, (uint8_t)read, buf); } } return rcode; } 再回到USB 逻辑分析仪抓取的结果中(有2次输出的温度结果): 结合这个温度计提供的二次开发的文档,可以得知温度的计算方法: 温度= 第三个字节+第四个字节的低4位 x0.0625 USB HOST 库提供了HID解析的框架,我们根据框架创建USB温度计的代码,关键部分是: 1. voidTemperReportParser: arse(HID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) 在这里取得每次获得的 HID数据,如果两次数据有差别,这意味着温度有变化,会通知temperEvents->OnTemperChanged; 2. voidTemperEvents::OnTemperChanged(const TemperEventData *evt) 用来将收到的HID数据转化为温度值,在这里我们直接使用Println从串口数据温度结果。 #include "temper_rptparser.h" TemperReportParser::TemperReportParser(TemperEvents*evt) : temperEvents(evt) { } void TemperReportParser: arse(HID *hid,bool is_rpt_id, uint8_t len, uint8_t *buf) { boolmatch = true; //Checking if there are changes in report since the method was last called for(uint8_t i=0; i<TEMPER_LEN; i++) { if(buf != oldPad ) { match= false; break; } } // Calling temper event handler if(!match && temperEvents) { temperEvents->OnTemperChanged((constTemperEventData*)buf); for(uint8_t i=0; i<TEMPER_LEN; i++) oldPad = buf; } } void TemperEvents::OnTemperChanged(constTemperEventData *evt) { float f; char t[10]; byte i; //for (i=0;i<8;i++) { // Serial.print(evt->data); // Serial.print(" "); // } f=evt->data[2]+(evt->data[3] >>4 & 0xF) * 0.0625; dtostrf(f,2,2,t); Serial.println(t); } 上述框架完成后,实际代码非常简单: #include <hid.h> #include <hiduniversal.h> #include <usbhub.h> #include "temper_rptparser.h" USB Usb; HIDUniversal Hid(&Usb); TemperEvents TemperEvents; TemperReportParser Temper(&TemperEvents); void setup() { Serial.begin( 115200); Serial.println("Start"); if (Usb.Init() ==-1) Serial.println("OSC did not start."); delay( 200 ); if(!Hid.SetReportParser(0, &Temper)) ErrorMessage<uint8_t>(PSTR("SetReportParser"), 1 ); } void loop() { Usb.Task(); } 最终运行结果: 对于绝大多数USB 设备来说,USB接口部分不会非常复杂,因为设计USB接口和驱动或者应用程序的不会是同一个人。复杂的接口对于他们来说会造成调试和沟通上的极大困难。因此,可以用多种方法来尝试解析数据通讯从而完成Arduino 对USB设备的控制。 对于 Arduino来说,有多种多样的温度传感器配件,获取环境温度是非常简单的事情,本文的主要目的是展示如何直接和USB设备进行通讯。随着时代的发展,很多测量设备使用USB 接口作为对外通讯的接口,Arduino如果能直接实现USB通讯,将会大大扩展Arduino 的使用范围。 参考:
|