Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

usb: device_next: Document if callbacks are mandatory #84584

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Thalley
Copy link
Collaborator

@Thalley Thalley commented Jan 27, 2025

Adds additional documentation to the callbacks of uac2_ops to describe if/when they are mandatory to register.

Additionally add asserts before calling them to help debugging if the user did not register the required ones.

@Thalley Thalley marked this pull request as ready for review January 28, 2025 07:19
@zephyrbot zephyrbot added the area: USB Universal Serial Bus label Jan 28, 2025
Copy link
Contributor

@tmon-nordic tmon-nordic left a comment

Choose a reason for hiding this comment

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

I would prefer to have the checks as early as possible, that is in usbd_uac2_set_ops().

@@ -262,6 +264,7 @@ int usbd_uac2_send(const struct device *dev, uint8_t terminal,
LOG_ERR("No endpoint for terminal %d", terminal);
return -ENOENT;
}
__ASSERT_NO_MSG(ctx->ops != NULL && ctx->ops->buf_release_cb != NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Checking for ctx->ops doesn't really make sense because uac2_init() will fail if ctx->ops is NULL which fails udc_init() which prevents the host from ever seeing the device.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This assumes that usbd_uac2_set_ops has always been called before usbd_enable. This isn't documented, nor does usbd_enable check for ctx->ops == NULL.

Should it be added as a requirement that usbd_uac2_set_ops is always called before usbd_enable, and usbd_enable verifies this? If so, then I agree that the above can be removed, but if not then ctx->ops can definitely be NULL.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, since the callbacks are specific to usbd_uac2, then such a check cannot happen in usbd_enable.

Do you have any suggestion on how to approach this? Just checking in usbd_uac2_set_ops isn't enough if we cannot ensure that usbd_uac2_set_ops has been called first

Copy link
Contributor

Choose a reason for hiding this comment

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

Why usbd_enable would have to check for ctx->ops == NULL if the udc_init() will fail the whole stack if the ops are not registered? If the whole stack fails on UDC, then the class won't ever be enabled because for that you need working control transfers (which do require working UDC).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why usbd_enable would have to check for ctx->ops == NULL if the udc_init() will fail the whole stack if the ops are not registered?

udc_init does not check if dev->ctx->ops is set. As far as I can tell, ctx->ops is only a thing in usbd_uac2.c, since the ctx struct, struct uac2_ctx is only defined in that file.

Copy link
Contributor

@tmon-nordic tmon-nordic Jan 31, 2025

Choose a reason for hiding this comment

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

udc_init does not check it directly, but usbd_init() does call uac2_init() in the process which does check ctx->ops. Returning -EINVAL in uac2_init() prevents the device from even presenting itself on the bus because usbd_device_init_core() will return an error.

@@ -436,6 +441,7 @@ void uac2_update(struct usbd_class_data *const c_data,
as_idx = iface - iad->bFirstInterface - 1;

/* Notify application about terminal state change */
__ASSERT_NO_MSG(ctx->ops != NULL && ctx->ops->terminal_update_cb != NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems redundant. Check in usbd_uac2_set_ops() should be enough.

@@ -800,6 +808,7 @@ static void uac2_sof(struct usbd_class_data *const c_data)
struct uac2_ctx *ctx = dev->data;
int as_idx;

__ASSERT_NO_MSG(ctx->ops != NULL && ctx->ops->sof_cb != NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems redundant. Check in usbd_uac2_set_ops() should be enough.

@@ -379,6 +383,7 @@ static void write_explicit_feedback(struct usbd_class_data *const c_data,
bi = udc_get_buf_info(buf);
bi->ep = ep;

__ASSERT_NO_MSG(ctx->ops != NULL && ctx->ops->feedback_cb != NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer to have the check in usbd_uac2_set_ops. The feedback_cb is necessary whether uac2_cfg (const struct uac2_cfg *cfg = dev->config;) array fb_indexes (range 0 to num_ifaces) has any non-zero value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Isn't feedback_cb optional if using implicit-feedback? It's in a function called write_explicit_feedback, which I assume is not called when using implicit feedback

Copy link
Contributor

Choose a reason for hiding this comment

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

If the sample uses implicit feedback, then there won't be any non-zero value in fb_indexes array.

@@ -328,6 +331,7 @@ static void schedule_iso_out_read(struct usbd_class_data *const c_data,
}

/* Prepare transfer to read audio OUT data from host */
__ASSERT_NO_MSG(ctx->ops != NULL && ctx->ops->get_recv_buf != NULL);
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer to have the check in usbd_uac2_set_ops.

It is possible to determine whether there are any ISO IN endpoints (and therefore get_recv_buf is necessary) by looking at all non-zero ep_indexes (range 0 to num_ifaces) entries in uac2_cfg (const struct uac2_cfg *cfg = dev->config;). For each non-zero ep_indexes value ('idx') check whether the corresponding struct usb_ep_descriptor bEndpointAddress is IN with USB_EP_DIR_IS_IN().

Check get_as_data_ep() for reference. The check wouldn't have to worry about as_idx, just check all ep_indexes from 0 to num_ifaces. If there are both fs_descriptors and hs_descriptors then it is sufficient to check only one of the descriptor sets.

The check can also be used for data_recv_cb - if the USB_EP_DIR_IS_IN() was false, then it means it was OUT and therefore there is at least one ISO OUT endpoint and thus data_recv_cb is mandatory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, I see that data_recv_cb is called only if if (USB_EP_DIR_IS_OUT(ep)) { - Did you mean USB_EP_DIR_IS_OUT in your comment? As far as I can tell data_recv_cb and get_recv_buf are used together, so shouldn't they check the same endpoint type?

Copy link
Contributor

Choose a reason for hiding this comment

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

Double negation in "if the USB_EP_DIR_IS_IN() was false" essentially means that USB_EP_DIR_IS_OUT() is true.

@Thalley
Copy link
Collaborator Author

Thalley commented Feb 3, 2025

@tmon-nordic @jfischer-no I've updated the PR to check only in usbd_uac2_set_ops. I've followed the existing design of using __ASSERT, but would it make sense to change usbd_uac2_set_ops to be able to return an error value instead?

@tmon-nordic
Copy link
Contributor

would it make sense to change usbd_uac2_set_ops to be able to return an error value instead?

I don't think so. What would the application do with the error code? Not providing appropriate callabacks is a programmer error and asserts are the right tool to catch programmer errors.

@Thalley
Copy link
Collaborator Author

Thalley commented Feb 3, 2025

Not providing appropriate callabacks is a programmer error and asserts are the right tool to catch programmer errors.

I think that's debatable :D But will leave that design up to the USB subsystem people (i.e. not me) :D

}

if (cfg->ep_indexes[i] != 0U) {
const struct usb_ep_descriptor *desc = get_as_data_ep(cfg->c_data, i);
Copy link
Contributor

Choose a reason for hiding this comment

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

This code must not call get_as_data_ep() because it does depend on actual bus speed (which is not available before the stack is initialized).

if (cfg->ep_indexes[i] != 0U) {
	if (cfg->fs_descriptors && cfg->fs_descriptors[cfg->ep_indexes[i]]) {
		desc = (const struct usb_ep_descriptor *)cfg->fs_descriptors[cfg->ep_indexes[i]];
	} else {
		desc = (const struct usb_ep_descriptor *)cfg->hs_descriptors[cfg->ep_indexes[i]];
	}
}

UAC2 currently supports only Full-Speed and/or High-Speed and hence the above snippet will set desc to valid pointer if cfg->ep_indexes[i] is not zero.

Copy link
Contributor

Choose a reason for hiding this comment

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

Turns out, I slipped here. The && cfg->fs_descriptors[cfg->ep_indexes[i]] check is unnecessary. If cfg->ep_indexes[i] is non-zero and if fs_descriptors is not NULL, then fs_descriptors[cfg->ep_indexes[i]] is non-NULL.

Comment on lines 242 to 255
if (USB_EP_DIR_IS_OUT(desc->bEndpointAddress)) {
__ASSERT(ops->get_recv_buf, "get_recv_buf is mandatory");
__ASSERT(ops->data_recv_cb, "data_recv_cb is mandatory");
} else if (USB_EP_DIR_IS_IN(desc->bEndpointAddress)) {
__ASSERT(ops->buf_release_cb,
"buf_release_cb is mandatory");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please change it to either

				if (USB_EP_DIR_IS_OUT(desc->bEndpointAddress)) {
					__ASSERT(ops->get_recv_buf, "get_recv_buf is mandatory");
					__ASSERT(ops->data_recv_cb, "data_recv_cb is mandatory");
				}

				if (USB_EP_DIR_IS_IN(desc->bEndpointAddress)) {
					__ASSERT(ops->buf_release_cb,
						 "buf_release_cb is mandatory");
				}

or

				if (USB_EP_DIR_IS_OUT(desc->bEndpointAddress)) {
					__ASSERT(ops->get_recv_buf, "get_recv_buf is mandatory");
					__ASSERT(ops->data_recv_cb, "data_recv_cb is mandatory");
				} else {
					__ASSERT(ops->buf_release_cb,
						 "buf_release_cb is mandatory");
				}

or

			if (desc != NULL && USB_EP_DIR_IS_OUT(desc->bEndpointAddress)) {
				__ASSERT(ops->get_recv_buf, "get_recv_buf is mandatory");
				__ASSERT(ops->data_recv_cb, "data_recv_cb is mandatory");
			}

			if (desc != NULL && USB_EP_DIR_IS_IN(desc->bEndpointAddress)) {
				__ASSERT(ops->buf_release_cb, "buf_release_cb is mandatory");
			}

I am confident that the compiler will produce equivalent results.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Applied the first suggestion

Adds additional documentation to the callbacks of uac2_ops
to describe if/when they are mandatory to register.

Additionally add asserts before calling them to help debugging
if the user did not register the required ones.

Signed-off-by: Emil Gydesen <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: USB Universal Serial Bus
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants