Skip to content

samples: subsystem: usb: udc: Add multi-instance initialization#103844

Closed
nandojve wants to merge 4 commits intozephyrproject-rtos:mainfrom
nandojve:usb/add_multi_instance_selection
Closed

samples: subsystem: usb: udc: Add multi-instance initialization#103844
nandojve wants to merge 4 commits intozephyrproject-rtos:mainfrom
nandojve:usb/add_multi_instance_selection

Conversation

@nandojve
Copy link
Copy Markdown
Member

This introduces a generic mechanism to instantiate multiple USB interfaces and enable/assign the classes automatically.

Problem:

Assuming that the hardware have multiple USB interfaces how we can select only the UDC instances?

Proposal:

Add a generic binding to be added in the interfaces to allow for use of FOR_EACH macros based in that bind. This allows us to use multi-vendor interfaces and group then together at build time.

Description:

This demonstrate how to use the binding in the CDC-ACM example.

The below snip add the binding in the two UDC interfaces. After that, the application can initialize looking all the zephyr,udc defined and active.

    zephyr_udc0: &usbotg_hs {
      compatible = "st,stm32-otghs", "zephyr,udc";
      ...
    };
    zephyr_udc1: &udp {
      compatible = "atmel,udc-sam-udp", "zephyr,udc";
      ...
    };

The classes now should be added as child of the respective interfaces.

&usbotg_hs {
	cdc_ncm_eth0: cdc_ncm_eth0 {
		compatible = "zephyr,cdc-ncm-ethernet";
	};
};

&udp {
	cdc_acm_uart0 {
		compatible = "zephyr,cdc-acm-uart";
	};
};

Using this approach, the user can control which interface is active and what is exposed on that interface.

The example demonstrate that no changes in the core stack are necessary to allow the binding to happen. Assuming that the USB-CORE can handle multiple instances per interface this could automate the initialization. The code was made to keep the compatibility with the zephyr_udc0 interface name and avoid double instantiation.

Notes:

Add zephyr,udc.yaml binding that can be used as a secondary compatible
on any USB device controller. This enables auto-discovery of all UDC
devices via DT_FOREACH_STATUS_OKAY(zephyr_udc, fn).

Signed-off-by: BUDKE Gerson Fernando <gerson.budke@leica-geosystems.com>
Add support for auto-discovering USB device controllers using the
zephyr,udc compatible string. This enables boards with multiple UDC
devices to have them all initialized automatically.

Changes to sample_usbd_init.c:
- Use DT_INST_FOREACH_STATUS_OKAY(zephyr_udc) to discover UDC devices
- Add fallback for legacy boards using zephyr_udc0 nodelabel without
  zephyr,udc compatible (backward compatibility)
- Add sample_usbd_get_device_count() to get discovered device count
- Add sample_usbd_get_context() to get USBD context by index
- Add sample_usbd_init_all_devices() to initialize all discovered UDCs

Changes to cdc_acm sample:
- Update enable_usb_device_next() to init all discovered UDC devices
- Enable each device that cannot detect VBUS

Signed-off-by: BUDKE Gerson Fernando <gerson.budke@leica-geosystems.com>
With the multi-UDC discovery it is possible to automatically discover
all the UDC device controller and automatically instances all those.
However the CDC-ACM sample currently only support one terminal in one
instance. This refactor the CDC-ACM sample to support multiple UART
instances simultaneously - one per controller. Each instance has its
own thread that waits for DTR independently, allowing connections to
be handled without blocking each other.

Architecture:

  main()
    - Init all CDC-ACM instances
    - Enable USB devices
    - Spawn thread per instance

         |
         +------------------+------------------+
         v                  v                  v
  +----------------+ +----------------+ +----------------+
  | Thread 0       | | Thread 1       | | Thread N       |
  | k_sem_take()   | | k_sem_take()   | | k_sem_take()   |
  | (wait for DTR) | | (wait for DTR) | | (wait for DTR) |
  |       |        | |       |        | |       |        |
  |       v        | |       v        | |       v        |
  | setup_instance | | setup_instance | | setup_instance |
  +----------------+ +----------------+ +----------------+
         ^                  ^                  ^
         |                  |                  |
  +------+------------------+------------------+----------+
  | sample_msg_cb() - gives semaphore to correct instance |
  +-------------------------------------------------------+

Changes:
- Add per-instance struct with ring buffer, semaphore, and thread
- Use DT_FOREACH_STATUS_OKAY to auto-discover CDC-ACM UART devices
- Spawn dedicated thread per instance to wait for DTR
- IRQ handler uses user_data to access per-instance data
- Each connection is handled independently

Signed-off-by: BUDKE Gerson Fernando <gerson.budke@leica-geosystems.com>
When multiple UDC devices are discovered via zephyr,udc compatible,
the sample previously used shared descriptors which caused conflicts.
The second device would fail with -ENOMEM/-EBUSY because descriptors
can only be attached to one USBD context.

This commit adds:
- Per-device descriptor instantiation using DT_INST_FOREACH macros
- Per-device configuration structures (sample_usbd_desc_set)
- Detection of UDC devices with USB class children via devicetree
- Skip class registration for UDCs without class children
- Informative log messages when class registration is skipped

This enables systems with multiple UDC devices to initialize correctly,
with classes only registered on devices that have USB class children
defined in devicetree.

Signed-off-by: BUDKE Gerson Fernando <gerson.budke@leica-geosystems.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
E Reliability Rating on New Code (required ≥ C)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copy link
Copy Markdown
Contributor

@jfischer-no jfischer-no left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add support for auto-discovering USB device controllers using the
zephyr,udc compatible string. This enables boards with multiple UDC
devices to have them all initialized automatically.

In very rare cases, more than one USB controller is available on the platform. I have never seen more than two. Use cases for two USB controller in device mode are even rarer. There are no differences in setting up and initializing the second controller/USB-device, and an application does not need to be aware of anything special. We do not need a sample to demonstrate this. We do not need a more complicated CDC ACM sample just to demonstrate this one very rare use case. A sample should not abuse and depend on DT compatible just to demonstrate this rare use case. There is no benefit to the user application from that. Application developers of these very rare use case know their platforms, they do not need to iterate over whatever is there. They know the nodelabels/node-id and can just pass them to the USBD_DEVICE_DEFINE macro.

@nandojve
Copy link
Copy Markdown
Member Author

Hi @jfischer-no ,

I think this is not about rare use case, it is about use cases. Microchip is bringing the dual instance here #99620 and we need to instantiate both controllers and we need to differentiate host vs device soon with multiple controllers. There are cases that one want to use with manageable USB-HUB controller like some of these https://www.microchip.com/en-us/products/interface-and-connectivity/usb/usb-hubs which can bind multiple devices in the same board to a host using only 1 VBUS, which lead to manage the up/down the stacks correctly.

This is not about a change on a sample it is about start the discussion about how to support these use cases. The infrastructure that was proposed here can be used together with the VBUS from #103840, which solve the main issue for the UDC use case and can be extended. If you see that this should be made differently it is fine but then let me know your opinion.

For instance, where we should bind a floating cdc_acm_uart0 node ? Will it be in the zephyr_udc0 or zephyr_udc1 ?
This exposes some points of improvements at least. Then, to make sure we are instantiation the correct things in the right place we need to solve these small details.

I think postpone these discussion will only increase the pressure in future.

@josuah
Copy link
Copy Markdown
Contributor

josuah commented Feb 15, 2026

Maybe the same way network interfaces provision their IP from Kconfig, using this possible workaround that can be used right away from the application:

#77638 (comment)

Whenever (and if) a "software provisioning" mechanism arrives to Zephyr, that allows such a thing to be used as a replacement for the application specific workaround.

This can also be a good way to explore and report how manageable it is.

Then it is also possible to set a lot of other related per-instance properties like VID:PID, interface names etc, given these are typically passed as strings in .c files, so easy to connect with Kconfig + i.e. LISTIFY().

Comment on lines +410 to +420
/* Only register classes for UDCs that have class children */
if (has_classes) {
err = usbd_register_all_classes(ctx, USBD_SPEED_FS, 1, blocklist);
if (err) {
LOG_ERR("Failed to register FS classes (%d)", err);
return err;
}
} else {
LOG_INF("Device %zu: Skipping FS class registration "
"(no USB class children in devicetree)", idx);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see how usbd_register_all_classes() would assign the classes correctly. From the PR, I get impression that all class instances would get registered for all UDC instances that have at least 1 class in devicetree node. This would for sure fail if multiple instances were used simultaneously.

It would even fail if not used simultaneously if the UDC instances have to use different endpoint numbers (e.g. one controller supports bulk only on endpoint X, the other only on endpoint Y) because the classes descriptors are not modified in any way here (I would prefer if the class descriptors were kept in ROM and not modified at runtime, but this is out of scope here).

@nandojve
Copy link
Copy Markdown
Member Author

Thanks @jfischer-no, @josuah and @tmon-nordic for the review.

@tmon-nordic, you're right. usbd_register_all_classes() iterates the global usbd_class_fs/usbd_class_hs linker sections and registers every class to whichever context calls it first. The second context gets -EBUSY because each class node can only bind to one uds_ctx. There is no way today to associate a class instance with a specific UDC controller.

This is actually the core problem I was trying to surface with this PR — I agree the sample was not the right vehicle for it.

@jfischer-no, I understand the concern about sample complexity and I'm happy to drop the CDC ACM changes. But I think tmon-nordic's comments highlights the real gap beyond the sample: even if a developer knows their node labels and calls usbd_register_class() explicitly, two CDC-ACM instances cannot be assigned to two different controllers simultaneously. The class data structure only holds a single uds_ctx pointer, and the USBD_CCTX_REGISTERED flag is per-node, not per-context.

This matters to make the correct adjustments:
1- Microchip dual-instance UDC (#99620)
2- Boards that want to bind multiple devices to a single VBUS to create a power-domain.
3- Host vs. Device differentiation

For reference, Linux's USB gadget configfs architecture solved this by scoping functions (classes) per-gadget and explicitly binding each gadget to a specific UDC controller. There is no global "register all functions" - each function is created within a gadget's namespace and explicitly linked to a configuration.

It is architecturally impossible to accidentally bind a function to the wrong controller. This is essentially what we're missing in Zephyr: a way to associate class instances with a specific UDC context. The current global linker section approach works for single-UDC but has no path to multi-UDC without adding some form of device affinity.

This PR goal was to expose this issues and have a point to open a more broad discussion to enable us to talk and find a good solution.

What would be the best way to start with these topics in your opinion?

@jfischer-no
Copy link
Copy Markdown
Contributor

@tmon-nordic, you're right. usbd_register_all_classes() iterates the global usbd_class_fs/usbd_class_hs linker sections and registers every class to whichever context calls it first. The second context gets -EBUSY because each class node can only bind to one uds_ctx. There is no way today to associate a class instance with a specific UDC controller.

It is not that the functions are randomly registered somewhere. The functions have names. Your application has full control over where to register these functions. And unregister them and move to another context. Whatever you want.

This is actually the core problem I was trying to surface with this PR — I agree the sample was not the right vehicle for it.

@jfischer-no, I understand the concern about sample complexity and I'm happy to drop the CDC ACM changes. But I think tmon-nordic's comments highlights the real gap beyond the sample: even if a developer knows their node labels and calls usbd_register_class() explicitly, two CDC-ACM instances cannot be assigned to two different controllers simultaneously.

It is not that I have concerns. It is that I am absolutely against it. It does not solve a "real gap" but it introduces a huge problem. We do not assign functions to controllers, but to an USB device context.

The class data structure only holds a single uds_ctx pointer, and the USBD_CCTX_REGISTERED flag is per-node, not per-context.

Not sure what you mean. But please, if you think there is something wrong, file a bug report.

This matters to make the correct adjustments: 1- Microchip dual-instance UDC (#99620) 2- Boards that want to bind multiple devices to a single VBUS to create a power-domain. 3- Host vs. Device differentiation

(2) just does not sounds like right way to do it using "single VBUS", who is bus-powered and who is self-powered.
(3) No idea what do you mean.

For reference, Linux's USB gadget configfs architecture solved this by scoping functions (classes) per-gadget and explicitly binding each gadget to a specific UDC controller. There is no global "register all functions" - each function is created within a gadget's namespace and explicitly linked to a configuration.

Nice, but look Zephyr is not Linux kernel. No idea why we should do something like that. You do not have to use "register all functions"!

It is architecturally impossible to accidentally bind a function to the wrong controller. This is essentially what we're missing in Zephyr: a way to associate class instances with a specific UDC context.

The current global linker section approach works for single-UDC but has no path to multi-UDC without adding some form of device affinity.

"global linker section" is not about "single-UDC", it is just to know what functions are there and allow them to be registered at context of your choice. So for your very unusual multi-UDC case, where multi<=2, please use usbd_register_class().

usbd_register_class(&usbd0, "loopback_0", USBD_SPEED_FS, 1);
...
usbd_register_class(&usbd1, "loopback_1", USBD_SPEED_FS, 1);
usbd_register_class(&usbd1, "cdc_acm_0", USBD_SPEED_FS, 2);

This PR goal was to expose this issues and have a point to open a more broad discussion to enable us to talk and find a good solution.

What would be the best way to start with these topics in your opinion?

I do not see any need for "these topics".

@tmon-nordic
Copy link
Copy Markdown
Contributor

tmon-nordic commented Feb 25, 2026

It is not that the functions are randomly registered somewhere. The functions have names. Your application has full control over where to register these functions. And unregister them and move to another context. Whatever you want.

It has full control over where the classes are registered, but while application can refer to CDC ACM UART by respective label in devicetree I can't see a easy way to correlate zephyr,cdc-acm-uart compatible with cdc_acm_0 and cdc_acm_1 if there are two zephyr,cdc-acm-uart instances.

@github-actions
Copy link
Copy Markdown

This pull request has been marked as stale because it has been open (more than) 30 days with no activity. Remove the stale label or add a comment saying that you would like to have the label removed otherwise this pull request will automatically be closed in 7 days. Note, that you can always re-open a closed pull request at any time.

@github-actions github-actions Bot added the Stale label Mar 28, 2026
@github-actions github-actions Bot closed this Apr 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: Boards/SoCs area: Devicetree Binding PR modifies or adds a Device Tree binding area: Devicetree area: Samples Samples area: USB Universal Serial Bus Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants