I don't look back,I always have plenty of reasons to look ahead!
我不会回头看,我总是有足够的理由向前看!
--舒马赫[1]
赛车是每一个男人的梦想,但是不是每一个人都能真正的摸一次赛车方向盘,买了尘埃拉力赛很久了,一直都是手柄玩,让我技术长得很慢!遂上网查外设,简直是暴利,直流有刷电机居然卖1000+,当然国产外设厂商起来以后,确实给方向盘外设打下一大截,不过还是囊中羞涩,在本科的时候,借助网上固件MMOS做过一次方向盘,但是,没有自己写程序总感觉东西不是自己的,这一次,查了一些资料,在国外论坛看到了一些教程,有阅读了一下usb相关协议,决定自己写一个驱动
这篇文章使用ESP32S3实现了一个力反馈设备驱动,目前在尘埃拉力赛2.0和欧卡上测试还比较正常,但是依然存在一些小的问题,此文仅以用来记录学习和探索的过程。
材料选型
主控选用esp32s3;电机驱动选用的是Odriver,使用CAN通信和电机通信。
参考资料
一、ESP32实现USB-HID设备
ESP32自带tinyUSB协议栈,结合示例程序能非常容易的实现USB设备,打开esp-idf的example 选择tusd-hid 示例程序
这是接收数据的回调函数:
// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen)
{
(void) instance;
(void) report_id;
(void) report_type;
(void) buffer;
(void) reqlen;
return 0;
}
// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize)
{
}
第一个回调函数是接收请求报告的时候进入的回调函数,
instance是实例编号,也就是usb的接口号,在我的实现中我只有一个USB接口所以基本为0,
report_id为报告ID,报告ID和你的报告描述符的设置有关系;
report_type 为报告类型,有三种,特性报告,输入报告,输出报告,不过这个中断函数几乎都是特性报告,因为这个中断函数是请求输入报告,即主机请求从机想主机发送特性的报告,例如查询刚刚加载的数据是否成功?查询内存是否充足?等等;
buffer 是数据缓冲,这个函数将请求的数据直接复制到缓冲器,然后将最后一个参数设置为返回的数据长度,并且最后函数要return 这个值;
bufsize为返回数据长度;
第二个回调函数的参数与上述一样
注意:这个参数仅仅在报告类型为Feature报告的时候才有效,输出报告的时候该参数一直为零,根据协议,原始数据的第零位是报告ID
USB-HID协议简述
USB-HID协议非常复杂,我也是因此项目初次接触,只能简单阐述一下这里面需要的知识,有错误之处,请多包涵;
同时我主要参考了《圈圈带你玩USB》一书以及B站up hoop0 的视频,推荐大家看一看原视频,深入浅出,讲的很好。
枚举过程
枚举过程由USB芯片自动完成,在制作过程中不必过分在意,但是了解枚举过程还是有助于了解其工作原理。在这里贴一篇博主的文章供参考
设备描述符
设备描述符(Device Descriptor)说明了USB设备的通用信息,包含应用到全部设备和所有设备配置的信息。USB设备只有一个设备描述符(Device Descriptor)。设备描述符是在设备连接时主机读取的第一个描述符。设备描述符所含的信息,被主机用来取得设备的额外内容。设备描述符(Device Descriptor)提供了关于设备、设备的配置以及任何设备所归属的类的信息。
| 字段名 | 长度(字节) | 说明 |
|---|---|---|
| bLength | 1 | 描述符总长度(固定为18字节) |
| bDescriptorType | 1 | 描述符类型(设备描述符固定为0x01) |
| bcdUSB | 2 | USB协议版本(BCD格式,如0x0200表示USB 2.0) |
| bDeviceClass | 1 | 设备类代码(例如:0x00表示接口定义类,0x03为HID类) |
| bDeviceSubClass | 1 | 设备子类代码(依赖主类,例如HID子类0x01为启动设备) |
| bDeviceProtocol | 1 | 设备协议(依赖类和子类,例如HID协议0x02表示鼠标) |
| bMaxPacketSize0 | 1 | 端点0的最大数据包大小(有效值:8, 16, 32, 64) |
| idVendor | 2 | 厂商ID(由USB-IF官方分配,例如0x1234) |
| idProduct | 2 | 产品ID(厂商自定义,例如0xABCD) |
| bcdDevice | 2 | 设备版本号(BCD格式,例如0x0100表示版本1.0) |
| iManufacturer | 1 | 厂商名称字符串的索引(0表示无字符串) |
| iProduct | 1 | 产品名称字符串的索引(0表示无字符串) |
| iSerialNumber | 1 | 序列号字符串的索引(0表示无字符串) |
| bNumConfigurations | 1 | 设备支持的配置数量(至少为1) |
一般 bDeviceClass bDeviceSubClass bDeviceProtocol 不写,即填充为0x00 因为这会限制后面的协议,而bInterfaceClass 中指定实现的功能
在ESP32的tinyUSB示例中
const tinyusb_config_t tusb_cfg = {
.device_descriptor = NULL,
.string_descriptor = hid_string_descriptor,
.string_descriptor_count = sizeof(hid_string_descriptor) / sizeof(hid_string_descriptor[0]),
.external_phy = false,
#if (TUD_OPT_HIGH_SPEED)
.fs_configuration_descriptor = hid_configuration_descriptor, // HID configuration descriptor for full-speed and high-speed are the same
.hs_configuration_descriptor = hid_configuration_descriptor,
.qualifier_descriptor = NULL,
#else
.configuration_descriptor = hid_configuration_descriptor,
#endif // TUD_OPT_HIGH_SPEED
};tinyusb配置结构体中device_descriptor 指针默认为NULL,这代表使用ESP32默认的设备描述符,如果要改变其ProduceID和VendorID可以在配置项(menuconfig)中修改
配置描述符集合
配置描述符涉及多个层次的描述符,包括配置描述符、接口描述符、HID描述符和端点描述符。
| 偏移量 | 字段 | 说明 |
|---|---|---|
| 0 | bLength | 固定长度 9 字节 |
| 1 | bDescriptorType | 描述符类型 (0x02) |
| 2 | wTotalLength | 整个集合的总长度(包括所有后续描述符) |
| 4 | bNumInterfaces | 此配置包含的接口数量 |
| 5 | bConfigurationValue | 配置标识符(用于 SetConfiguration 请求) |
| 6 | iConfiguration | 配置字符串索引(0=无) |
| 7 | bmAttributes | 属性位掩码: |
Bit7=1(保留) Bit6=自供电 Bit5=远程唤醒 |
||
| 8 | bMaxPower | 最大功耗(单位:2mA) |
接口描述符
| 字段 | 长度 | 说明 | 示例值 |
|---|---|---|---|
bLength |
1 | 描述符长度,固定为 0x09 |
0x09 |
bDescriptorType |
1 | 描述符类型,固定为 0x04(接口描述符) |
0x04 |
bInterfaceNumber |
1 | 接口编号(从 0 开始) | 0x00 |
bAlternateSetting |
1 | 备用接口编号(通常为 0x00) |
0x00 |
bNumEndpoints |
1 | 接口使用的端点数(不含控制端点 0) | 0x01 |
bInterfaceClass |
1 | 接口类:HID 设备为 0x03 |
0x03 |
bInterfaceSubClass |
1 | 子类:0x01=支持启动协议,0x00=无 |
0x00 |
bInterfaceProtocol |
1 | 协议:0x01=键盘,0x02=鼠标,0x00=无 |
0x02 |
iInterface |
1 | 接口字符串索引(0x00 表示无) |
0x00 |
HID 描述符结构 (HID Descriptor)
| 偏移量 | 字段 | 长度(字节) | 值类型 | 说明 |
|---|---|---|---|---|
0 |
bLength |
1 | 数字 | 描述符长度(最小为 9,根据附加描述符增加) |
1 |
bDescriptorType |
1 | 常量 | 描述符类型:HID 描述符(固定为 0x21) |
2 |
bcdHID |
2 | BCD | HID 规范版本号(如 0x0111 表示 HID 1.11) |
4 |
bCountryCode |
1 | 数字 | 国家/地区代码: 0x00=不支持本地化 0x01=阿拉伯 0x02=比利时 0x07=日本 0x21=美国 |
5 |
bNumDescriptors |
1 | 数字 | 附属描述符数量(至少为 1,即报告描述符) |
6 |
bDescriptorType1 |
1 | 常量 | 附属描述符类型: 0x22=报告描述符(必需) 0x23=物理描述符 |
7 |
wDescriptorLength1 |
2 | 数字 | 附属描述符长度(单位:字节) |
9 |
bDescriptorType2 |
1 | 常量 | 可选:第二个附属描述符类型(当 bNumDescriptors ≥ 2) |
10 |
wDescriptorLength2 |
2 | 数字 | 可选:第二个附属描述符长度 |
| ... | ... | ... | ... | 更多附属描述符(每增加一个重复后两列) |
关键字段说明
bcdHID- 采用 BCD 编码表示 HID 规范版本
- 常见值:
0x0110(HID 1.10),0x0111(HID 1.11)
bNumDescriptors- 至少包含 报告描述符 (
0x22) - 示例:
0x02= 报告描述符 + 物理描述符
- 至少包含 报告描述符 (
附属描述符类型
0x22: 报告描述符 (Report Descriptor) - 定义设备数据格式 0x23: 物理描述符 (Physical Descriptor) - 描述物理操作部件
USB 配置描述符结构 (Configuration Descriptor)
| 偏移量 | 字段 | 长度(字节) | 值类型 | 说明 |
|---|---|---|---|---|
0 |
bLength |
1 | 数字 | 描述符长度(固定为 9 字节) |
1 |
bDescriptorType |
1 | 常量 | 描述符类型:配置描述符(固定为 0x02) |
2 |
wTotalLength |
2 | 数字 | 配置信息总长度(包括后续所有接口/端点描述符) |
4 |
bNumInterfaces |
1 | 数字 | 接口数量(该配置支持的接口数) |
5 |
bConfigurationValue |
1 | 数字 | 配置标识符(用于 SetConfiguration 请求选择此配置) |
6 |
iConfiguration |
1 | 索引 | 配置字符串索引(描述该配置的字符串描述符索引,0=无字符串) |
7 |
bmAttributes |
1 | 位掩码 | 配置特性: |
8 |
bMaxPower |
1 | 数字 | 最大功耗(单位:2mA,如 50=100mA) |
关键字段详解
wTotalLength- 包含当前配置下所有描述符的总长度(配置+接口+端点+类特定描述符)
- 示例:
0x0022= 总长度 34 字节
bmAttributes位掩码- 0x80: 保留位(必须为1)
- 0x40: 自供电(1=是,0=否)
- 0x20: 远程唤醒支持(1=支持)
- 0x1F: 保留位(设为0)
在ESP32IDF示例中,并不是一一填写这些描述符,他已经提供了固定的描述符配置,仅需要填写关键的信息,在文usbd.h 中提供了大量的宏定义来提供这些描述符,在使用实例中,描述符被定义为
static const uint8_t hid_configuration_descriptor[] = {
// Configuration number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
// Interface number, string index, boot protocol, report descriptor len, EP OUT address,EP In address, size & polling interval
TUD_HID_INOUT_DESCRIPTOR(0, 4, false, FFB_DESC_LENGTH, 0x02,0x81, 64, 5),
};TUD_CONFIG_DESCRIPTOR 是tinyusb 写好的宏函数,扩展为配置描述符,只需要填入对应的参数即可TUD_HID_INOUT_DESCRIPTOR 则是一个大杂烩包含了剩下描述符(不包含报告描述符和字符串描述符)同样只需要填入参数即可
最后配置描述符集合hid_configuration_descriptor[]
这个东西比较复杂,我基本参考网上的描述符,但是都大同小异,无非是数据长度的差异,但是我是用的描述符有一些问题(我也不知道是不是文件描述符的问题),使用此描述符的设备无法在我的笔记本电脑上被识别为力反馈设备。但是我提供了对应的补丁(一些注册表文件)
该描述符可通过文件我的开源代码下载...
力反馈设备相关协议
简单说明了我们需要填写的USB描述符以后,现在开始介绍正式的力反馈设备相关的协议,参考资料是hid《usage-table》的Physic Input Device Page(0x0F)页
力反馈设备的力反馈是由11种预设效果合成的,参数块的数量等于关节数量乘以轴数,或等于 2,以较大值为准,最小值 2 可支持一个包络参数块和一个额外的特定类型参数块。具体种类参见下表(usage table )
效果类型主要分为恒定效果,斜坡效果,方波效果,正弦波,三角波效果,锯齿波(向上),锯齿波(向下),弹簧效果,阻尼效果,惯性效果,摩擦效果以及自定义。自定义可以忽略,我查阅基本所有的都能没有实现自定义效果
而参数块基本可以分为,恒定参数,包络参数,包络参数主要实现的是淡入淡出的效果,周期参数块和条件参数快,条件参数主要实现的是弹簧,摩擦,阻尼,惯性等效果
力反馈设备的通信主要是下载效果,设置效果的参数,打开效果,关闭效果等等,下面开始详细解释一下各个包的含义,注意:我在下文说的报告描述符ID是我写在报告描述符里面的,不是代表这个包的ID一定是这么多,取决于你的描述符。
创建新的效果,描述符ID 为0x10,类型是一个设置特性报告。特性报告第第0位是效果的类型,但是在这里我没保存,因为后面设置参数的时候还有对应的参数,这里不需要管。
创建效果的状态,描述符ID 为0x11,类型是一个获取特性报告。主机向设备请求一个报告,里面包含了上一次创建效果的结果,创建的效果ID,因为我们选择的是从机管理内存,所以我们要返回对应的效果块的ID,以后的效果块的指代都由ID
所以创建效果块的流程就是
1.主机发送创建效果块的设置特性报告,从机处理(分配内存,分配效果ID)
2.主机发送创先效果块的结果的获取特性报告,从机回复(效果块的ID,是否创建成功,剩余内存为多少)
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize)
{
switch (report_type)
{
case HID_REPORT_TYPE_FEATURE: //report id 1 set effect
//printf("set report_id %d\n",report_id);
if(report_id == CREATE_NEW_EFFECT_REPORT){ //如果是创建效果块则进入创建效果块的回调函数里面
CreateNewEffect(report_id,buffer,bufsize);
}else if(report_id == CONFIG_REPORT_ID){
Config(buffer,bufsize);
}
break;
case HID_REPORT_TYPE_OUTPUT:
HandleOutput(buffer,bufsize);
break;
default:
break;
}
}
//回复的包的数据结构
typedef struct
{
uint8_t effect_block_index; // index dell'effetto
uint8_t block_load_status; // 1 ok, 2 -out of memory, 3 JC was here, or maybe not ? case: undefined.
uint16_t ram_pool_available;
}PidBlockLoadReport;
PidBlockLoadReport _pidBlockLoadReport;
void CreateNewEffect(uint8_t reportID,const uint8_t *hid_report_out,uint8_t length)
{
uint8_t _blockIndex = GetNextIndex(); //获取效果块的ID
_pidBlockLoadReport.effect_block_index = _blockIndex; //构建回复包,等会要用
if(_blockIndex!=0){
_pidBlockLoadReport.block_load_status = SUCCESS;//构建回复包,等会要用
_pidBlockLoadReport.ram_pool_available = (MAX_EFFECTS_NUM - _ramPool.UsedCount)*sizeof(Force_t); //构建回复包,等会要用
_ramPool.ForcesPool[_blockIndex-1].State = MEFFECTSTATE_ALLOCATED;
_ramPool.UsedCount++;
//printf("Create New Effect Type %d, Block Index %d\n",hid_report_out[0],_blockIndex);
}
else{//如果创建失败,则构建回复包
_pidBlockLoadReport.block_load_status = OUT_OF_MEMORY;
_pidBlockLoadReport.ram_pool_available = 0x00;
}
}[未完待续...]
[1] 迈克尔·舒马赫,德国F1传奇车手,“车王”。职业生涯7次夺得F1世界冠军(1994, 1995, 2000-2004),创多项纪录。2013年滑雪重伤昏迷,经长期治疗转入居家护理,健康状况未公开。