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-idfexample 选择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 数字 可选:第二个附属描述符长度
... ... ... ... 更多附属描述符(每增加一个重复后两列)

关键字段说明

  1. bcdHID

    • 采用 BCD 编码表示 HID 规范版本
    • 常见值:0x0110 (HID 1.10), 0x0111 (HID 1.11)
  2. bNumDescriptors

    • 至少包含 报告描述符 (0x22)
    • 示例:0x02 = 报告描述符 + 物理描述符
  3. 附属描述符类型
    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)

关键字段详解

  1. wTotalLength

    • 包含当前配置下所有描述符的总长度(配置+接口+端点+类特定描述符)
    • 示例:0x0022 = 总长度 34 字节
  2. 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_DESCRIPTORtinyusb 写好的宏函数,扩展为配置描述符,只需要填入对应的参数即可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年滑雪重伤昏迷,经长期治疗转入居家护理,健康状况未公开。

源码面前无秘密