为了让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类设备分两大类:
USB HID类设备使用相应的HID类驱动来检索和路由数据。数据的路由和检索是通过检查设备描述符及其提供的数据来完成的。
##2.2. HID类设备描述符
HID类设备描述符定义了HID类描述符的数量和长度,例如报告描述符和物理描述符。
报告描述符描述了设备产生的数据的方方面面,以及什么样的数据是正在监控的。通过检查items,HID类驱动程序可以确定来自HID类设备的数据报告的大小和组成。
物理描述符集是可选的描述符,它提供有关用于激活设备上的控件的人体部位的信息。
上述HID描述符是设备描述符结构整体中的一部分:
##2.3. HID类接口描述符
HID有四种功能特性:
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),工具界面如下:
用这个工具可以生成面向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的HID报告描述符,可以观察到其中包含2个报告:
PC端通过读取HID类接口描述符获取HID类端点描述符以建立合适的通信管道。每个接口支持最多16个端点。其中端点0是默认控制端口,即每个USB HID接口都有端点0,其余15个端口可以配置成其他传输模式。
HID类端点描述符的主要属性如下:
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. 搭建环境
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. 总结
#6. 参考文章