CSR8675学习笔记:USB HID通信

为了让CSR867x的开发更容易,现与思度科技联合推出CSR867x学习板【淘宝链接:思度科技CSR开发板】。

技术交流QQ群号:743434463
开发板会员QQ群号:725398389(凭订单号入群,赠PPT、项目源码、视频教程)
——————————正文分割线———————————–

#1. 引言
常见的PC端与CSR8675的通信方式有USB HID和UART这两种。UART通信方式简单,但在产品结构上需预留专门的硬件接口,给ID设计带来不便。USB HID通信可以与USB音频播放、USB充电功能共用一个硬件接口,是较理想的通信方式。

#2. 基本概念
##2.1. USB HID
Universal Serial Bus(USB)是一种4线连接的通信接口,用于PC与不同设备间的通信互联。这些设备分为不同的类。每种设备有着共同的行为和协议,以提供相似的功能,例如:
|设备类|示例设备|
|:|:|
|显示|监视器|
|通信|调制器|
|音频|扬声器|
|存储|硬盘|
|人机接口|键盘|

Human Interface Device(HID)类设备用来由人控制电脑系统的运行,典型的HID类设备分两大类:

  • 键盘、鼠标、按键、开关、旋钮、进度条、遥控器等与人交互的设备
  • 一些不需要人参与交互,但有着与HID类设备相似的数据格式的设备

USB HID类设备使用相应的HID类驱动来检索和路由数据。数据的路由和检索是通过检查设备描述符及其提供的数据来完成的。
这里写图片描述
##2.2. HID类设备描述符
HID类设备描述符定义了HID类描述符的数量和长度,例如报告描述符和物理描述符。
CSR8675学习笔记:USB HID通信_第1张图片

报告描述符描述了设备产生的数据的方方面面,以及什么样的数据是正在监控的。通过检查items,HID类驱动程序可以确定来自HID类设备的数据报告的大小和组成。
CSR8675学习笔记:USB HID通信_第2张图片

物理描述符集是可选的描述符,它提供有关用于激活设备上的控件的人体部位的信息。
CSR8675学习笔记:USB HID通信_第3张图片

上述HID描述符是设备描述符结构整体中的一部分:
CSR8675学习笔记:USB HID通信_第4张图片
##2.3. HID类接口描述符
HID有四种功能特性:

  • Class(类):HID的Class必须是3
  • SubClass(子类):0-不支持Boot设备,1-支持Boot设备,
  • Protocol(协议):仅当SubClass为1时有效,0-None,1-键盘,2-鼠标
  • Interface(接口):控制(Endpoint 0),中断输入,中断输出

CSR8675的接口描述符如下:

#define B_INTERFACE_CLASS_HID 0x03
#define B_INTERFACE_SUB_CLASS_HID_NO_BOOT 0x00
#define B_INTERFACE_PROTOCOL_HID_NO_BOOT 0x00     
#define I_INTERFACE_INDEX 0x00

static const UsbCodes usb_codes_hid_no_boot = {B_INTERFACE_CLASS_HID, /* bInterfaceClass */
                                               B_INTERFACE_SUB_CLASS_HID_NO_BOOT, /* bInterfaceSubClass */
                                               B_INTERFACE_PROTOCOL_HID_NO_BOOT, /* bInterfaceProtocol */
                                               I_INTERFACE_INDEX /* iInterface */
                                               };

其中的I_INTERFACE_INDEX指的是当前接口描述符对应的字符串描述符的索引号,CSR8675支持16个字符串描述符。可在PSKEY中修改:
这里写图片描述
##2.4. HID类报告描述符
HID类报告描述符定义了通过HID设备传输的数据的格式,官方提供了简易工具用于查看、编辑和保存HID类报告描述符(官方下载链接:HID Descriptor Tool),工具界面如下:
CSR8675学习笔记:USB HID通信_第5张图片
用这个工具可以生成面向C的代码,方便实现自定义的HID类报告描述符。CSR8675的HID类报告描述符的代码如下:

typedef struct {
    uint8 report_id;
    uint8 command;
    uint8 data[1021];
} hid_command_t;

typedef struct {
    uint8 report_id;
    uint8 last_command;        
    uint8 last_command_status;
} hid_status_t;

#define REPORT_COMMAND_ID       1
#define REPORT_COMMAND_SIZE     ((sizeof(hid_command_t)/sizeof(uint8))-1)
#define REPORT_STATUS_ID        2
#define REPORT_STATUS_SIZE      ((sizeof(hid_status_t)/sizeof(uint8))-1)

/*
 HID Report Descriptor - HID Control Device */
static const uint8 report_descriptor_hid_control[] = 
{   0x06, 0x00, 0xff,              /* USAGE_PAGE (Vendor Defined Page 1) */
    0x09, 0x01,                    /* USAGE (Vendor Usage 1) */
    0xa1, 0x01,                    /* COLLECTION (Application) */
    0x15, 0x80,                    /*   LOGICAL_MINIMUM (-128) */
    0x25, 0x7f,                    /*   LOGICAL_MAXIMUM (127) */
    0x85, 0x01,                    /*   REPORT_ID (1) */
    0x09, 0x02,                    /*   USAGE (Vendor Usage 2) */
    0x96,                          /*   REPORT_COUNT */
    (REPORT_COMMAND_SIZE&0xff), 
    (REPORT_COMMAND_SIZE>>8), 
    0x75, 0x08,                    /*   REPORT_SIZE (8) */
    0x91, 0x02,                    /*   OUTPUT (Data,Var,Abs) */
    0x85, 0x02,                    /*   REPORT_ID (2) */
    0x09, 0x02,                    /*   USAGE (Vendor Usage 2) */
    0x95,                          /*   REPORT_COUNT */
    (REPORT_STATUS_SIZE&0xff),
    0x75, 0x08,                    /*   REPORT_SIZE (8) */
    0x81, 0x02,                    /*   INPUT (Data,Var,Abs) */
    /*0xb1, 0x02,*/
    
    0xc0                           /* END_COLLECTION */
}

用工具生成的C代码如下:

char ReportDescriptor[33] = {
    0x06, 0x00, 0xff,              // USAGE_PAGE (Vendor Defined Page 1)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x15, 0x80,                    //   LOGICAL_MINIMUM (-128)
    0x25, 0x7f,                    //   LOGICAL_MAXIMUM (127)
    0x85, 0x01,                    //   REPORT_ID (1)
    0x09, 0x02,                    //   USAGE (Vendor Usage 2)
    0x96, 0xff, 0x03,              //   REPORT_COUNT (1023)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
    0x85, 0x02,                    //   REPORT_ID (2)
    0x09, 0x02,                    //   USAGE (Vendor Usage 2)
    0x95, 0x03,                    //   REPORT_COUNT (3)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0xc0                           // END_COLLECTION

可见两者格式基本一致。为了便于理解HID报告描述符的数据格式,给出一般的数据结构如下:
CSR8675学习笔记:USB HID通信_第6张图片

  • 每个报告描述符中包含1个Application Collection
  • 每个Application Collection中包含多个Report
  • 每个Report包含1组描述,包括报告数据的位数(Report Size),数据的长度(Report Count)等
  • 每个Report可对应多个用途(Usage)

结合CSR8675的HID报告描述符,可以观察到其中包含2个报告:

  • 报告1:共1024字节(报告ID占1字节+数据占1023字节),HID设备接收此报告
  • 报告2:共4字节(报告ID占1字节+数据占3字节),HID设备发送此报告
    ##2.5. HID类端点描述符
    PC端与HID设备端的数据传输基于“管道——端点“”机制,示意图如下:
    CSR8675学习笔记:USB HID通信_第7张图片
    PC端的一个线程是一个管道的起点,通过USB线对接HID设备端的一个端点。

PC端通过读取HID类接口描述符获取HID类端点描述符以建立合适的通信管道。每个接口支持最多16个端点。其中端点0是默认控制端口,即每个USB HID接口都有端点0,其余15个端口可以配置成其他传输模式。

HID类端点描述符的主要属性如下:

  • 端点地址:共16个地址,以1字节表示。bit 7:1-to host,0- to device;bit 0-3:地址。
  • 端点传输模式: 端点有4种传输模式,分别是控制传输模式、批量传输模式、同步传输模式和中断传输模式。不同的传输模式适用于不同的设备类型。控制传输模式适用于PC端作为主控制台,批量传输模式适用于文件传输,同步传输模式适用于音视频数据传输,中断模式适用于实时性强的控制和数据采集。
  • 最大数据包大小:端口处理数据的能力。不同的控制传输模式对应的最大数据包大小不一样。
  • 轮询间隔:访问端口数据缓冲区的时间间隔。

CSR8675的HID端点描述符如下:

#define end_point_int_out   (0x81) /*!< Interrupt ToHost */
#define end_point_bulk_in   (0x02) /*!< Bulk FromHost */
#define end_point_bulk_out  (0x82) /*!< Bulk ToHost */
#define end_point_iso_in    (0x03) /*!< Isochronous FromHost */
#define end_point_iso_out   (0x83) /*!< Isochronous ToHost */
#define end_point_int_out2  (0x85) /*!< Interrupt ToHost */
#define end_point_bulk_in2  (0x06) /*!< Bulk FromHost */
#define end_point_bulk_out2 (0x86) /*!< Bulk ToHost */
#define end_point_iso_in2   (0x07) /*!< Isochronous FromHost */
#define end_point_int_out3  (0x89) /*!< Interrupt ToHost */
#define end_point_bulk_in3  (0x0A) /*!< Bulk FromHost */
#define end_point_bulk_out3 (0x8A) /*!< Bulk ToHost */
#define end_point_int_out4  (0x8D) /*!< Interrupt ToHost */
#define end_point_bulk_in4  (0x0E) /*!< Bulk FromHost */
#define end_point_bulk_out4 (0x8E) /*!< Bulk ToHost */

typedef enum
{
    end_point_attr_ctl = 0,    /*!< Control.*/
    end_point_attr_iso = 1,    /*!< Isochronous.*/
    end_point_attr_bulk = 2,   /*!< Bulk.*/
    end_point_attr_int = 3,    /*!< Interrupt.*/
    end_point_attr_iso_sync = 13  /*!< Isochronous & Synchronisation Type Synchronous (bits 3:2 = 11) */
} EndPointAttr;

/* USB HID endpoint information */
static const EndPointInfo epinfo_hid_control_transport[] =
{
    {        
        end_point_int_out, /* address */
        end_point_attr_int, /* attributes */
        16, /* max packet size */
        1, /* poll_interval */
        0, /* data to be appended */
        0, /* length of data appended */
    },
    {        
        end_point_bulk_in, /* address */
        end_point_attr_int, /* attributes */
        64, /* max packet size */
        1, /* poll_interval */
        0, /* data to be appended */
        0, /* length of data appended */
    }
};

可以看到描述了两个HID端口,一个用于向PC端发送数据,工作在中断传输模式;另一个用于接收PC端的数据,工作在批量传输模式。

#3. USB HID通信(设备端)
##3.1. 配置PSKEY
将下列PSKEY配置通过PSTool工具写入CSR8675的内部Flash:

&0001 = 0000 1213 005b 0002
// PSKEY_USB_DATA_PLUS_PULL_CONTROL
&01f0 = 0001// sets D+ when configuration is done (when ready)
// PSKEY_HOST_INTERFACE
&01f9 = 0002// USB link
// PSKEY_USB_DEVICE_CLASS_CODES
&02bd = 0000 0000 0000
// PSKEY_USB_PRODUCT_ID
&02bf = 1243
// PSKEY_USB_PIO_VBUS
&02d1 = fffe// Use VDD_CHG (battery charger)
// PSKEY_USB_CONFIG
&02d9 = 0038
// PSKEY_USB_ALLOW_DEEP_SLEEP
&02fc = 0003
// PSKEY_USB_VM_CONTROL
&03c0 = 0001// True
// PSKEY_ONCHIP_HCI_CLIENT
&03cc = 0001
// PSKEY_INITIAL_BOOTMODE
&03cd = 0001

上述配置是为了确保CSR8675的USB的描述符由VM层设定。另一个关键点是要确保boot mode 1的专属PSKEY段没有覆盖上述PSKEY值。

##3.2. 枚举设备
CSR8675作为USB HID device,需要在上电时完成枚举动作:

static const usb_device_class_hid_control_config usb_hid_config_control =
{
    {interface_descriptor_hid_control_transport,
    sizeof(interface_descriptor_hid_control_transport),
    epinfo_hid_control_transport},
    {report_descriptor_hid_control,
    sizeof(report_descriptor_hid_control),
    NULL}
};

static bool usbEnumerateHidControl(void)
{
    if (!usb_hid_control_config)
    {
        usb_hid_control_config = &usb_hid_config_control;        
        PRINT(("USB: HID control default descriptors\n"));
    }
    
    device->usb_interface[usb_interface_hid_control] = UsbAddInterface(&usb_codes_hid_no_boot, B_DESCRIPTOR_TYPE_HID, usb_hid_control_config->interface.descriptor, usb_hid_control_config->interface.size_descriptor);
    if (device->usb_interface[usb_interface_hid_control] == usb_interface_error)
        return FALSE;

    /* Register HID Control Device report descriptor with the interface */
    PRINT(("USB: HID control UsbAddDescriptor\n"));
    if (UsbAddDescriptor(device->usb_interface[usb_interface_hid_control], B_DESCRIPTOR_TYPE_HID_REPORT, usb_hid_control_config->report.descriptor, usb_hid_control_config->report.size_descriptor) == FALSE)
        return FALSE;

    /* Add required endpoints to the interface */
    PRINT(("USB: HID control UsbAddEndPoints\n"));
    if (UsbAddEndPoints(device->usb_interface[usb_interface_hid_control], 2, usb_hid_control_config->interface.end_point_info) == FALSE)
        return FALSE;
    
    device->usb_task[usb_task_hid_control].handler = hidControlHandler;
    (void) VmalMessageSinkTask(StreamUsbClassSink(device->usb_interface[usb_interface_hid_control]), &device->usb_task[usb_task_hid_control]);
    (void) VmalMessageSinkTask(StreamUsbEndPointSink(end_point_bulk_in), &device->usb_task[usb_task_hid_control]);
    
    return TRUE;
}

上述代码中可以看到,初始化USB HID时需要用到HID接口描述符、报告描述符、端口描述符,且将hidControlHandler作为USB sink的消息处理函数。

##3.3. 下行数据接收(host to device)
hidControlHandler用来与PC端通过HID接口交换数据。其源码如下:

static void hidControlHandler(Task task, MessageId id, Message message)
{
    MessageMoreData *msg = (MessageMoreData*)message;
    uint16 packet_size;
    hid_status_t status_report;
    const uint8 *in;
    
    if (id == MESSAGE_MORE_DATA)
    {
        PRINT(("USB: MESSAGE_MORE_DATA hid consumer\n"));
        if (msg->source == StreamUsbClassSource(device->usb_interface[usb_interface_hid_control]))
        {
            handleHidClassRequest(StreamUsbClassSource(device->usb_interface[usb_interface_hid_control]), USB_DEVICE_CLASS_TYPE_HID_CONTROL);
        }
        else if (msg->source == USB_SOURCE)
        {
            while ((packet_size = SourceBoundary(msg->source)) != 0) 
            {
                in = SourceMap(msg->source);
                PRINT(("USB MORE INT DATA: %d\n",packet_size));
                PRINT(("command: %d\n",((hid_command_t*)in)->command));

                status_report.report_id = REPORT_STATUS_ID;
                status_report.last_command = ((hid_command_t*)in)->command;
                status_report.last_command_status= STATUS_CMD_FAILED;

                SourceDrop(msg->source, packet_size);
                HidSendStatus(&status_report);
            }
        }
    }
}

##3.4. 上行数据发送(device to host)
USB源收到新的数据后,hidControlHandler会收到MESSAGE_MORE_DATA消息。此时判断USB数据源是默认端口0还是端口end_point_bulk_in。如果是端口end_point_bulk_in,读取端口数据并调用HidSendStatus(&status_report)返回消息状态。HidSendStatus源码如下:

/* send a status report over the interrupt endpoint */
static void HidSendStatus(hid_status_t *status_report)
{
    Sink sink = StreamUsbEndPointSink(end_point_int_out);
    uint8 *out;
    
    if ((out = claimSink(sink, sizeof(hid_status_t))) != 0) 
    {
        PRINT(("Last command status=%d\n",status_report->last_command_status)); 
        memmove(out, status_report,  sizeof(hid_status_t));
        PRINT(("USB sending %d bytes\n", sizeof(hid_status_t))); 
        PanicFalse(SinkFlush(sink, sizeof(hid_status_t)));
    }
    else
    {
        PRINT(("USB cannot claim sink space\n")); 
    }
}

StreamUsbEndPointSink(end_point_int_out)的意思是将消息状态数据通过USB端口end_point_int_out发送给PC端程序。

#4. USB HID通信(PC端)
Windows平台为USB HID提供了通用的API支持,实现与HID类设备间的USB接口通信。用VC++编写应用程序调用此API,即可方便地实现定制化的USB HID功能开发。

##4.1. 搭建环境

  • 下载安装WinDDK(官方链接)
  • 新建Win32项目,参考如下配置修改工程属性:

    
      Disabled
      WIN32;_DEBUG;_CONSOLE;_DDK_;%(PreprocessorDefinitions)
      true
      Default
      MultiThreadedDebugDLL
      Use
      Level3
      EditAndContinue
      $(WDKPATH)\inc\ddk;$(WDKPATH)\inc\api;$(WDKPATH)\inc\crt;D:\WinDDK\7600.16385.1\inc\ddk;D:\WinDDK\7600.16385.1\inc\api;D:\WinDDK\7600.16385.1\inc\crt;%(AdditionalIncludeDirectories)
      1Byte
      false
      true
      Cdecl
      false
    
    
      Setupapi.lib;Hid.lib;%(AdditionalDependencies)
      true
      Console
      MachineX86
      $(WDKPATH)\lib\win7\i386;D:\WinDDK\7600.16385.1\lib\win7\i386;%(AdditionalLibraryDirectories)
      false
      NotSet
      
      
      false
      false
      true
    
    
      false
    
  

##4.2. 查询目标HID类设备
尝试打开HID类设备:

/* returns handle when device found or NULL when not found */
HANDLE OpenDevice(void) {
	wchar_t device_path[MAX_PATH];
	HANDLE DeviceHandle;
	if (EnumerateDevices(device_path)) {
		/* create handle to the device */
		DeviceHandle=CreateFile(device_path, 
				 				GENERIC_READ | GENERIC_WRITE, 
								FILE_SHARE_READ|FILE_SHARE_WRITE, 
								(LPSECURITY_ATTRIBUTES)NULL,
								OPEN_EXISTING, 
								FILE_ATTRIBUTE_NORMAL, 
								NULL);
		if (DeviceHandle!=INVALID_HANDLE_VALUE) {
			return(DeviceHandle);
		}
	}
	return(NULL);
}

枚举设备时会查询每个HID设备的接口描述符中的Product ID、Vendor ID、HID类报告描述符的Usage Page和Usage属性值是否与目标HID设备的相符。当检查到匹配设备后,返回设备的句柄DeviceHandle。

int EnumerateDevices(wchar_t *device_path) {
	SP_DEVICE_INTERFACE_DATA devInfoData;
	int MemberIndex;
	ULONG Length;
	GUID HidGuid;
	HANDLE hDevInfo;
	HANDLE LocDevHandle;
	HIDD_ATTRIBUTES Attributes;
	PSP_DEVICE_INTERFACE_DETAIL_DATA detailData;
	PHIDP_PREPARSED_DATA PreparsedData;
	HIDP_CAPS Capabilities;
	int result=0;

	/* get HID GUID */
	HidD_GetHidGuid(&HidGuid);
	/* get pointer to the device information */
	hDevInfo = SetupDiGetClassDevs(&HidGuid,
			  					   NULL, 
								   NULL, 
								   DIGCF_PRESENT|DIGCF_DEVICEINTERFACE);
	/* go through all the device infos and find devices we are interested in */
	devInfoData.cbSize = sizeof(devInfoData);
	MemberIndex = 0;
	while((SetupDiEnumDeviceInterfaces(hDevInfo, 
								      0, 
									  &HidGuid, 
									  MemberIndex, 
									  &devInfoData))&&(result==0)) {
		/* first get the size of memory needed to hold the device interface info */
		SetupDiGetDeviceInterfaceDetail(hDevInfo, 
		   							    &devInfoData, 
										NULL, 
										0, 
										&Length, 
										NULL);
		/* allocate memory */
		detailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(Length);
		/* and set the size in the structure */
		detailData -> cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
		/* now get the actual device interface info */
		SetupDiGetDeviceInterfaceDetail(hDevInfo, 
										&devInfoData, 
										detailData, 
										Length, 
										NULL, 
										NULL);
#ifdef DEBUG
		wprintf(L"%s\n",detailData->DevicePath);
#endif
		/* create handle to the device */
		LocDevHandle=CreateFile(detailData->DevicePath, 
				 				GENERIC_READ | GENERIC_WRITE, 
								FILE_SHARE_READ|FILE_SHARE_WRITE, 
								(LPSECURITY_ATTRIBUTES)NULL,
								OPEN_EXISTING, 
								FILE_ATTRIBUTE_NORMAL, 
								NULL);
		/* set the size in the structure */
		Attributes.Size = sizeof(Attributes);
		/* get and test the VID and PID */
		HidD_GetAttributes(LocDevHandle,&Attributes);
		if ((Attributes.ProductID == 0x1243) &&
			(Attributes.VendorID == 0xa12)) {
			/* found the right device */
			/* is it the right HID collection? */
			HidD_GetPreparsedData(LocDevHandle, &PreparsedData);
			HidP_GetCaps(PreparsedData, &Capabilities);
#if 1
			wprintf(L"%04x %04x\n",Capabilities.UsagePage,Capabilities.Usage);
#endif
			if ((Capabilities.UsagePage == 0xFF00) &&
				(Capabilities.Usage == 0x0001)) {
					/* this is the correct HID collection */
				if (device_path!=NULL) {
					wcscpy(device_path,detailData->DevicePath);
				}
#ifdef DEBUG				
				wprintf(L"Device Found\n");
#endif
				result=1;
			}
		}
		/* close the device handle again */
		CloseHandle(LocDevHandle);
		/* and free the memory used to hold device info */
		free(detailData);
		/* try the next device */
		MemberIndex++;
	}
	/* free memory used for the device information set */
	SetupDiDestroyDeviceInfoList(hDevInfo);
	return result;
}

##4.3. 下行数据发送(host to device)
PC端调用API函数向device发送数据:

	/* reboot to bootmode 0 */
	command_report.report_id=REPORT_COMMAND_ID;
	command_report.command=COMMAND_NOP;
	command_report.data[0]=0x00;
	status_response.last_command_status=-1;
	if (!WriteFile(DeviceHandle,&command_report,sizeof(hid_command_t),&count,NULL)) {
		/* cannot write */
		return(FALSE);
	}

这里的REPORT_COMMAND_ID与CSR8675程序中定义的值相同。

##4.4. 上行数据接收(device to host)
PC端调用API函数查询接收device的上行数据:

	/* wait for response */
	if (!ReadFile(DeviceHandle,&status_response,sizeof(hid_status_t),&count,NULL)) {
		/* cannot read */
		return(FALSE);
	}
	wprintf(L"Response is %d.\n", status_response.last_command_status);

这里需要注意的是,如果上行数据未能发送成功,程序会一直阻塞在ReadFile函数,不能往下执行。容易犯的错误是,设备端未按照HID类报告描述符中规定的数据格式发送数据。

#5. 总结

  • 调试过程遇到问题时,可借助Bus Hound工具捕捉PC端的USB HID通信数据包来分析定位问题。
  • CSR8675的PID和VID存储在PSKEY中,可使用PSTool工具修改。

#6. 参考文章

  • HID读写过程
  • C++ USB HID host 示例代码
  • USB HID协议中几个关键概念的理解
  • Device Class Definition for HID 1.11
  • Tutorial about USB HID Report Descriptors
  • USB端点描述符
  • USB4种传输类型和端点
  • USB描述符详解

你可能感兴趣的:(蓝牙方案,硬件驱动,CSR8670蓝牙芯片软件开发)