USB Device Class support in Zephyr is being reworked. Three UDC (USB Device Controller) drivers exist already, and a skeleton for adding more. Next to that there are a number of classes that exist already and many more that need to be ported from the old infrastructure.
The idea is that the Zephyr application sees a virtual UART interface and is not involved in the underlying CDC ACM class. Similarly, the CDC ACM class is done on an abstract UDC API and is not involved with the specific UDC driver. This API is called the USBD Class API.
The class provides the USB interface descriptors and endpoint descriptors to the API. These are provided by the class. The application however is responsible to instantiate one or more configurations. Each configuration exposes a set of interfaces. The application also adds global descriptors like manufacturer and product. The application must also initialise a specific UDC device instance, register the configuration on it, and enable it.
For testing and development, there is also shell support for all these operations. You can also use PyUSB for testing. And there is a virtual UDC driver that allows testing without need physical stuff.
The class API (i.e. the interface between the class driver and the UDC driver) has handlers for initialization, shutdown, enabling and disabling (called when the configuration is selected), configuration update, and finally control and transfer request (i.e. the actual operation). To be added are handlers for power states.
Some class drivers can be described in device tree. For these, DT_INST_FOREACH_STATUS_OKAY can be used in the driver to instantiate the class based on DT. Alternatively, an instantiation macro can be called from the application.
Transfer requests are implemented by the class driver submitting usbd_ep_enqueue(). When the transfer is completed, a transfer request completed handler is called, e.g. to release buffers.