usb device custom HID device descriptor and interface

Hello, 

for a development project I need to make a USB device HID.
I started from the example project: \v2.6.0\zephyr\samples\subsys\usb\hid
so on version 2.6.0 of zephyr.

I have several things that I can't do from there... I need to do my own configuration. 

I would like to be able to create my own device descriptor, with these interfaces.. but when i add

CONFIG_USB_DEVICE_HID=y in the prj.conf file in order to use the libraries, a device descriptor with a config, an interface and an endpoint is created (device/class/hid/core.c )

ow to remove the interface present in the device/class/hid/core.c file?
I would like to add my own interfaces..

thanks for help

Parents
  • Hi

    Are you referring to the following code: https://github.com/nrfconnect/sdk-zephyr/blob/v3.5.99-ncs1/subsys/usb/device/class/hid/core.c#L52-L132 

    If so, it's just a matter of disabling the CONFIG_USB_HID_BOOT_PROTOCOL. Would it be a problem to add your own interfaces separately to the already existing ones? I don't see how they would be exclusive to one another.

    Best regards,

    Simon

  • ok I can possibly leave the example interface, I'll see if it can work like this. I need to simulate an existing device, so I really wanted to clean up the code to be identical to the existing device...


    To add my interfaces, I created a .c file in which I used the macros:

    USBD_CLASS_DESCR_DEFINE and USBD_DEFINE_CFG_DATA (see attached file)
    /**
     *****************************************************************************
     * @file       usb_device_custom_hid_if.c
     * @brief      Template of module implementation
     * @author     Temple8
     * @copyright  SDATAWAY
     *
     * History:
     * 2024.03.27  T8  Initial version
     *
     *****************************************************************************
     */
    /* Includes -----------------------------------------------------------------*/
    #include "usb_device_custom_hid_if.h"
    
    #include <zephyr/init.h>
    
    #include <zephyr/usb/usb_device.h>
    #include <zephyr/usb/class/usb_hid.h>
    #include <zephyr/sys/byteorder.h>
    #include <zephyr/sys/util.h>
    #include <usb_descriptor.h>
    
    #include <zephyr/logging/log.h>
    
    /* Global variables ---------------------------------------------------------*/
    
    /* Private constants --------------------------------------------------------*/
    #define LOG_LEVEL LOG_LEVEL_DBG
    LOG_MODULE_REGISTER(usbCustomIf);
    
    #define CUSTOM_81_EP_ADDR		0x81
    
    #define CUSTOM_82_EP_ADDR		0x82
    
    #define CUSTOM_83_EP_ADDR		0x83
    #define CUSTOM_03_EP_ADDR		0x03
    
    /* Private macros -----------------------------------------------------------*/
    
    /* Private typedefs ---------------------------------------------------------*/
    struct usb_hid_class_subdescriptor {
    	uint8_t bDescriptorType;
    	uint16_t wDescriptorLength;
    } __packed;
    
    struct usb_hid_descriptor {
    	uint8_t bLength;
    	uint8_t bDescriptorType;
    	uint16_t bcdHID;
    	uint8_t bCountryCode;
    	uint8_t bNumDescriptors;
    
    	/*
    	 * Specification says at least one Class Descriptor needs to
    	 * be present (Report Descriptor).
    	 */
    	struct usb_hid_class_subdescriptor subdesc[1];
    } __packed;
    
    struct usb_custom_if0_config {
    	struct usb_if_descriptor if0;
    	struct usb_hid_descriptor if0_hid;
    	struct usb_ep_descriptor if0_81_ep;
    } __packed;
    
    struct usb_custom_if1_config {
    	struct usb_if_descriptor if1;
    	struct usb_hid_descriptor if1_hid;
    	struct usb_ep_descriptor if1_82_ep;
    } __packed;
    
    struct usb_custom_if2_config {
    	struct usb_if_descriptor if2;
    	struct usb_hid_descriptor if2_hid;
    	struct usb_ep_descriptor if2_83_ep;
    	struct usb_ep_descriptor if2_03_ep;
    } __packed;
    
    USBD_CLASS_DESCR_DEFINE(primary, 0) struct usb_custom_if2_config custom_if2_cfg = {
    	// Interface descriptor 2 
    	.if2 = {
    		.bLength = sizeof(struct usb_if_descriptor),
    		.bDescriptorType = USB_DESC_INTERFACE,
    		.bInterfaceNumber = 0,
    		.bAlternateSetting = 0,
    		.bNumEndpoints = 2,
    		.bInterfaceClass = USB_BCC_HID,
    		.bInterfaceSubClass = 0,
    		.bInterfaceProtocol = 0,
    		.iInterface = 0,
    	},
      .if2_hid = {
    		.bLength = sizeof(struct usb_hid_descriptor),
    		.bDescriptorType = USB_DESC_HID,
    		.bcdHID = sys_cpu_to_le16(USB_HID_VERSION),
    		.bCountryCode = 0,
    		.bNumDescriptors = 1,
    		.subdesc[0] = {
    			.bDescriptorType = USB_DESC_HID_REPORT,
    			.wDescriptorLength = 30,
    		},
    	},	
    	// Data Endpoint 83 
    	.if2_83_ep = {
    		.bLength = sizeof(struct usb_ep_descriptor),
    		.bDescriptorType = USB_DESC_ENDPOINT,
    		.bEndpointAddress = CUSTOM_83_EP_ADDR,
    		.bmAttributes = USB_DC_EP_INTERRUPT,
    		.wMaxPacketSize = sys_cpu_to_le16(64),
    		.bInterval = 0x01,
    	},
    	// Data Endpoint 03 
    	.if2_03_ep = {
    		.bLength = sizeof(struct usb_ep_descriptor),
    		.bDescriptorType = USB_DESC_ENDPOINT,
    		.bEndpointAddress = CUSTOM_03_EP_ADDR,
    		.bmAttributes = USB_DC_EP_INTERRUPT,
    		.wMaxPacketSize = sys_cpu_to_le16(64),
    		.bInterval = 0x01,
    	},
    };
    
    USBD_CLASS_DESCR_DEFINE(primary, 0) struct usb_custom_if1_config custom_if1_cfg = {
    	// Interface descriptor 1 
    	.if1 = {
    		.bLength = sizeof(struct usb_if_descriptor),
    		.bDescriptorType = USB_DESC_INTERFACE,
    		.bInterfaceNumber = 0,
    		.bAlternateSetting = 0,
    		.bNumEndpoints = 1,
    		.bInterfaceClass = USB_BCC_HID,
    		.bInterfaceSubClass = 0,
    		.bInterfaceProtocol = 0,
    		.iInterface = 0,
    	},
      .if1_hid = {
    		.bLength = sizeof(struct usb_hid_descriptor),
    		.bDescriptorType = USB_DESC_HID,
    		.bcdHID = sys_cpu_to_le16(USB_HID_VERSION),
    		.bCountryCode = 0,
    		.bNumDescriptors = 1,
    		.subdesc[0] = {
    			.bDescriptorType = USB_DESC_HID_REPORT,
    			.wDescriptorLength = 84,
    		},
    	},	
    	// Data Endpoint 82
    	.if1_82_ep = {
    		.bLength = sizeof(struct usb_ep_descriptor),
    		.bDescriptorType = USB_DESC_ENDPOINT,
    		.bEndpointAddress = CUSTOM_82_EP_ADDR,
    		.bmAttributes = USB_DC_EP_INTERRUPT,
    		.wMaxPacketSize = sys_cpu_to_le16(64),
    		.bInterval = 0x01,
    	},
    };
    
    USBD_CLASS_DESCR_DEFINE(primary, 0) struct usb_custom_if0_config custom_if0_cfg = {
    	// Interface descriptor 0 
    	.if0 = {
    		.bLength = sizeof(struct usb_if_descriptor),
    		.bDescriptorType = USB_DESC_INTERFACE,
    		.bInterfaceNumber = 0,
    		.bAlternateSetting = 0,
    		.bNumEndpoints = 1,
    		.bInterfaceClass = USB_BCC_HID,
    		.bInterfaceSubClass = 0,
    		.bInterfaceProtocol = 0,
    		.iInterface = 0,
    	},
      .if0_hid = {
    		.bLength = sizeof(struct usb_hid_descriptor),
    		.bDescriptorType = USB_DESC_HID,
    		.bcdHID = sys_cpu_to_le16(USB_HID_VERSION),
    		.bCountryCode = 0,
    		.bNumDescriptors = 1,
    		.subdesc[0] = {
    			.bDescriptorType = USB_DESC_HID_REPORT,
    			.wDescriptorLength = 101,
    		},
    	},	
    	// Data Endpoint 81 
    	.if0_81_ep = {
    		.bLength = sizeof(struct usb_ep_descriptor),
    		.bDescriptorType = USB_DESC_ENDPOINT,
    		.bEndpointAddress = CUSTOM_81_EP_ADDR,
    		.bmAttributes = USB_DC_EP_INTERRUPT,
    		.wMaxPacketSize = sys_cpu_to_le16(64),
    		.bInterval = 0x01,
    	},
    };
    
    static struct usb_ep_cfg_data ep_83_cfg[] = {
    	{
    		.ep_cb = NULL,
    		.ep_addr = CUSTOM_83_EP_ADDR,
    	},	
    	{
    		.ep_cb = NULL,
    		.ep_addr = CUSTOM_03_EP_ADDR,
    	},
    };
    
    static struct usb_ep_cfg_data ep_82_cfg[] = {
    	{
    		.ep_cb = NULL,
    		.ep_addr = CUSTOM_82_EP_ADDR,
    	},
    };
    
    static struct usb_ep_cfg_data ep_81_cfg[] = {
    	{
    		.ep_cb = NULL,
    		.ep_addr = CUSTOM_81_EP_ADDR,
    	},
    };
    
    /* Private variables --------------------------------------------------------*/
    
    /* Private prototypes -------------------------------------------------------*/
    
    /* Public functions ---------------------------------------------------------*/
    
    /* Private functions --------------------------------------------------------*/
    static void custom_interface0_config(struct usb_desc_header *head,
    				      uint8_t bInterfaceNumber)
    {
    	ARG_UNUSED(head);
    
    	custom_if0_cfg.if0.bInterfaceNumber = bInterfaceNumber;
    }
    
    static void custom_interface1_config(struct usb_desc_header *head,
    				      uint8_t bInterfaceNumber)
    {
    	ARG_UNUSED(head);
    
    	custom_if1_cfg.if1.bInterfaceNumber = bInterfaceNumber;
    }
    
    static void custom_interface2_config(struct usb_desc_header *head,
    				      uint8_t bInterfaceNumber)
    {
    	ARG_UNUSED(head);
    
    	custom_if2_cfg.if2.bInterfaceNumber = bInterfaceNumber;
    }
    
    USBD_DEFINE_CFG_DATA(usb_custom_if2_config) = {
    	.usb_device_description = NULL,
    	.interface_config = custom_interface2_config,
    	.interface_descriptor = &custom_if2_cfg.if2,
    	.cb_usb_status = NULL,
    	.interface = {
    		.class_handler = NULL,
    		.custom_handler = NULL,
    		.vendor_handler = NULL,
    	},
    	.num_endpoints = ARRAY_SIZE(ep_83_cfg),
    	.endpoint = ep_83_cfg,
    };
    
    USBD_DEFINE_CFG_DATA(usb_custom_if1_config) = {
    	.usb_device_description = NULL,
    	.interface_config = custom_interface1_config,
    	.interface_descriptor = &custom_if1_cfg.if1,
    	.cb_usb_status = NULL,
    	.interface = {
    		.class_handler = NULL,
    		.custom_handler = NULL,
    		.vendor_handler = NULL,
    	},
    	.num_endpoints = ARRAY_SIZE(ep_82_cfg),
    	.endpoint = ep_82_cfg,
    };
    
    USBD_DEFINE_CFG_DATA(usb_custom_if0_config) = {
    	.usb_device_description = NULL,
    	.interface_config = custom_interface0_config,
    	.interface_descriptor = &custom_if0_cfg.if0,
    	.cb_usb_status = NULL,
    	.interface = {
    		.class_handler = NULL,
    		.custom_handler = NULL,
    		.vendor_handler = NULL,
    	},
    	.num_endpoints = ARRAY_SIZE(ep_81_cfg),
    	.endpoint = ep_81_cfg,
    };
    is this the right method? the interfaces seem well added when I look with USB Device viewer. but the device manager window detects a problem:
    the last point.. I want to add an IN endpoint at address 0x03, but it automatically takes the value 0x01 given that it is the only IN ep. can we set the address to 0x03?
  • Hi sarah_sdw,

    My apology for the late follow-up.

    sarah_sdw said:
    ok I can possibly leave the example interface, I'll see if it can work like this. I need to simulate an existing device, so I really wanted to clean up the code to be identical to the existing device...

    If that is your purpose, then please feel free to disable the built-in interface. It is meant to help common use case and be disabled when they are not desired.

    sarah_sdw said:

    To add my interfaces, I created a .c file in which I used the macros:

    USBD_CLASS_DESCR_DEFINE and USBD_DEFINE_CFG_DATA (see attached file)

    I thought you want to make an HID device with a custom descriptor? The macros you mentioned here are for implementing a custom USB class. If you just want to add custom descriptors, you should use the HID APIs instead. You can refer to the USB HID sample for some references:
    Docs: USB HID (Human Interface Device) — Zephyr Project documentation (nRF Connect SDK) (nordicsemi.com)
    Source: sdk-zephyr/samples/subsys/usb/hid/src/main.c at v3.5.99-ncs1 · nrfconnect/sdk-zephyr (github.com)

    sarah_sdw said:
    the last point.. I want to add an IN endpoint at address 0x03, but it automatically takes the value 0x01 given that it is the only IN ep. can we set the address to 0x03?

    It seems the documentation was updated after the latest NCS release (v2.6.0) with some notes about this exact problem.

    From (non-perma) link: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/zephyr/connectivity/usb/device/usb_device.html#interface-number-and-endpoint-address-assignment:

    Interface and endpoint descriptors of built-in USB class/function implementations in Zephyr RTOS typically have default interface numbers and endpoint addresses assigned in ascending order. During initialization, default interface numbers may be reassigned based on the number of interfaces in a given configuration. Endpoint addresses are reassigned based on controller capabilities, since certain endpoint combinations are not possible with every controller, and the number of interfaces in a given configuration. This also means that the device side class/function in the Zephyr RTOS must check the actual interface and endpoint descriptor values at runtime. This mechanism also allows as to provide generic samples and generic multifunction samples that are limited only by the resources provided by the controller, such as the number of endpoints and the size of the endpoint FIFOs.

    There may be host drivers for a specific function, for example in the Linux Kernel, where the function driver does not read interface and endpoint descriptors to check interface numbers or endpoint addresses, but instead uses hardcoded values. Therefore, the host driver cannot be used in a generic way, meaning it cannot be used with different device controllers and different device configurations in combination with other functions. This may also be because the driver is designed for a specific hardware and is not intended to be used with a clone of this specific hardware. On the contrary, if the driver is generic in nature and should work with different hardware variants, then it must not use hardcoded interface numbers and endpoint addresses. It is not possible to disable endpoint reassignment in Zephyr RTOS, which may prevent you from implementing a hardware-clone firmware. Instead, if possible, the host driver implementation should be fixed to use values from the interface and endpoint descriptor.

Reply
  • Hi sarah_sdw,

    My apology for the late follow-up.

    sarah_sdw said:
    ok I can possibly leave the example interface, I'll see if it can work like this. I need to simulate an existing device, so I really wanted to clean up the code to be identical to the existing device...

    If that is your purpose, then please feel free to disable the built-in interface. It is meant to help common use case and be disabled when they are not desired.

    sarah_sdw said:

    To add my interfaces, I created a .c file in which I used the macros:

    USBD_CLASS_DESCR_DEFINE and USBD_DEFINE_CFG_DATA (see attached file)

    I thought you want to make an HID device with a custom descriptor? The macros you mentioned here are for implementing a custom USB class. If you just want to add custom descriptors, you should use the HID APIs instead. You can refer to the USB HID sample for some references:
    Docs: USB HID (Human Interface Device) — Zephyr Project documentation (nRF Connect SDK) (nordicsemi.com)
    Source: sdk-zephyr/samples/subsys/usb/hid/src/main.c at v3.5.99-ncs1 · nrfconnect/sdk-zephyr (github.com)

    sarah_sdw said:
    the last point.. I want to add an IN endpoint at address 0x03, but it automatically takes the value 0x01 given that it is the only IN ep. can we set the address to 0x03?

    It seems the documentation was updated after the latest NCS release (v2.6.0) with some notes about this exact problem.

    From (non-perma) link: https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/zephyr/connectivity/usb/device/usb_device.html#interface-number-and-endpoint-address-assignment:

    Interface and endpoint descriptors of built-in USB class/function implementations in Zephyr RTOS typically have default interface numbers and endpoint addresses assigned in ascending order. During initialization, default interface numbers may be reassigned based on the number of interfaces in a given configuration. Endpoint addresses are reassigned based on controller capabilities, since certain endpoint combinations are not possible with every controller, and the number of interfaces in a given configuration. This also means that the device side class/function in the Zephyr RTOS must check the actual interface and endpoint descriptor values at runtime. This mechanism also allows as to provide generic samples and generic multifunction samples that are limited only by the resources provided by the controller, such as the number of endpoints and the size of the endpoint FIFOs.

    There may be host drivers for a specific function, for example in the Linux Kernel, where the function driver does not read interface and endpoint descriptors to check interface numbers or endpoint addresses, but instead uses hardcoded values. Therefore, the host driver cannot be used in a generic way, meaning it cannot be used with different device controllers and different device configurations in combination with other functions. This may also be because the driver is designed for a specific hardware and is not intended to be used with a clone of this specific hardware. On the contrary, if the driver is generic in nature and should work with different hardware variants, then it must not use hardcoded interface numbers and endpoint addresses. It is not possible to disable endpoint reassignment in Zephyr RTOS, which may prevent you from implementing a hardware-clone firmware. Instead, if possible, the host driver implementation should be fixed to use values from the interface and endpoint descriptor.

Children
  • thanks for reply but it's not entirely clear to me.

    when you say

    If that is your purpose, then please feel free to disable the built-in interface. It is meant to help common use case and be disabled when they are not desired.

    I would like to but I don't know how? 

    I need to make a custom hid, that's why I used macros. 

    i think my custom descriptors is ok.

  • sarah_sdw said:

    when you say

    If that is your purpose, then please feel free to disable the built-in interface. It is meant to help common use case and be disabled when they are not desired.

    I would like to but I don't know how? 

    Please refer to the code that Simon linked initially. You can find that the Endpoints and the Boot Protocols setup are within a #if check, which is based on Kconfigs.

    The Kconfigs relevant here are CONFIG_USB_HID_BOOT_PROTOCOL and CONFIG_ENABLE_HID_INT_OUT_EP. You can just disable them.

Related