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類描述符的數量和長度,例如報告描述符和物理描述符。
報告描述符描述了裝置產生的資料的方方面面,以及什麼樣的資料是正在監控的。通過檢查items,HID類驅動程式可以確定來自HID類裝置的資料報告的大小和組成。
物理描述符集是可選的描述符,它提供有關用於啟用裝置上的控制元件的人體部位的資訊。
上述HID描述符是裝置描述符結構整體中的一部分:
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),工具介面如下:
用這個工具可以生成面向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報告描述符的資料格式,給出一般的資料結構如下:
- 每個報告描述符中包含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裝置端的資料傳輸基於“管道——端點“”機制,示意圖如下:
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專案,參考如下配置修改工程屬性:
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;_DDK_;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MinimalRebuild>true</MinimalRebuild>
<BasicRuntimeChecks>Default</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>EditAndContinue</DebugInformationFormat>
<AdditionalIncludeDirectories>$(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)</AdditionalIncludeDirectories>
<StructMemberAlignment>1Byte</StructMemberAlignment>
<BufferSecurityCheck>false</BufferSecurityCheck>
<FunctionLevelLinking>true</FunctionLevelLinking>
<CallingConvention>Cdecl</CallingConvention>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<AdditionalDependencies>Setupapi.lib;Hid.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Console</SubSystem>
<TargetMachine>MachineX86</TargetMachine>
<AdditionalLibraryDirectories>$(WDKPATH)\lib\win7\i386;D:\WinDDK\7600.16385.1\lib\win7\i386;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<IgnoreAllDefaultLibraries>false</IgnoreAllDefaultLibraries>
<Driver>NotSet</Driver>
<EntryPointSymbol>
</EntryPointSymbol>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
<DataExecutionPrevention>false</DataExecutionPrevention>
<GenerateMapFile>true</GenerateMapFile>
</Link>
<ProjectReference>
<LinkLibraryDependencies>false</LinkLibraryDependencies>
</ProjectReference>
</ItemDefinitionGroup>
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工具修改。