You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1049 lines
29 KiB
1049 lines
29 KiB
/* |
|
* Copyright (c) 2023-2024 Nordic Semiconductor ASA |
|
* |
|
* SPDX-License-Identifier: Apache-2.0 |
|
*/ |
|
|
|
#include <stddef.h> |
|
#include <stdint.h> |
|
|
|
#include <zephyr/kernel.h> |
|
#include <zephyr/sys/__assert.h> |
|
#include <zephyr/sys/atomic.h> |
|
#include <zephyr/sys/byteorder.h> |
|
|
|
#include <zephyr/usb/usbd.h> |
|
#include <zephyr/usb/usb_ch9.h> |
|
#include <zephyr/usb/class/usbd_uac2.h> |
|
#include <zephyr/drivers/usb/udc.h> |
|
|
|
#include "usbd_uac2_macros.h" |
|
|
|
#include <zephyr/logging/log.h> |
|
LOG_MODULE_REGISTER(usbd_uac2, CONFIG_USBD_UAC2_LOG_LEVEL); |
|
|
|
#define DT_DRV_COMPAT zephyr_uac2 |
|
|
|
#define COUNT_UAC2_AS_ENDPOINT_BUFFERS(node) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_audio_streaming), ( \ |
|
+ AS_HAS_ISOCHRONOUS_DATA_ENDPOINT(node) + \ |
|
+ AS_IS_USB_ISO_IN(node) /* ISO IN double buffering */ + \ |
|
AS_HAS_EXPLICIT_FEEDBACK_ENDPOINT(node))) |
|
#define COUNT_UAC2_EP_BUFFERS(i) \ |
|
+ DT_PROP(DT_DRV_INST(i), interrupt_endpoint) \ |
|
DT_INST_FOREACH_CHILD(i, COUNT_UAC2_AS_ENDPOINT_BUFFERS) |
|
#define UAC2_NUM_EP_BUFFERS DT_INST_FOREACH_STATUS_OKAY(COUNT_UAC2_EP_BUFFERS) |
|
|
|
/* Net buf is used mostly with external data. The main reason behind external |
|
* data is avoiding unnecessary isochronous data copy operations. |
|
* |
|
* Allow up to 6 bytes per item to facilitate optional interrupt endpoint (which |
|
* requires 6 bytes) and feedback endpoint (4 bytes on High-Speed, 3 bytes on |
|
* Full-Speed). Because the total number of endpoints is really small (typically |
|
* there will be just 2 isochronous endpoints; the upper bound originating from |
|
* the USB specification itself is 30 non-control endpoints). Therefore, the |
|
* "wasted memory" here is likely to be smaller than the memory overhead for |
|
* more complex "only as much as needed" schemes (e.g. heap). |
|
*/ |
|
UDC_BUF_POOL_DEFINE(uac2_pool, UAC2_NUM_EP_BUFFERS, 6, |
|
sizeof(struct udc_buf_info), NULL); |
|
|
|
/* 5.2.2 Control Request Layout */ |
|
#define SET_CLASS_REQUEST_TYPE 0x21 |
|
#define GET_CLASS_REQUEST_TYPE 0xA1 |
|
|
|
/* A.14 Audio Class-Specific Request Codes */ |
|
#define CUR 0x01 |
|
#define RANGE 0x02 |
|
#define MEM 0x03 |
|
|
|
/* A.17.1 Clock Source Control Selectors */ |
|
#define CS_SAM_FREQ_CONTROL 0x01 |
|
#define CS_CLOCK_VALID_CONTROL 0x02 |
|
|
|
#define CONTROL_ATTRIBUTE(setup) (setup->bRequest) |
|
#define CONTROL_ENTITY_ID(setup) ((setup->wIndex & 0xFF00) >> 8) |
|
#define CONTROL_SELECTOR(setup) ((setup->wValue & 0xFF00) >> 8) |
|
#define CONTROL_CHANNEL_NUMBER(setup) (setup->wValue & 0x00FF) |
|
|
|
typedef enum { |
|
ENTITY_TYPE_INVALID, |
|
ENTITY_TYPE_CLOCK_SOURCE, |
|
ENTITY_TYPE_INPUT_TERMINAL, |
|
ENTITY_TYPE_OUTPUT_TERMINAL, |
|
} entity_type_t; |
|
|
|
static size_t clock_frequencies(struct usbd_class_data *const c_data, |
|
const uint8_t id, const uint32_t **frequencies); |
|
|
|
/* UAC2 device runtime data */ |
|
struct uac2_ctx { |
|
const struct uac2_ops *ops; |
|
void *user_data; |
|
/* Bit set indicates the AudioStreaming interface has non-zero bandwidth |
|
* alternate setting active. |
|
*/ |
|
atomic_t as_active; |
|
atomic_t as_queued; |
|
atomic_t as_double; |
|
uint32_t fb_queued; |
|
}; |
|
|
|
/* UAC2 device constant data */ |
|
struct uac2_cfg { |
|
struct usbd_class_data *const c_data; |
|
const struct usb_desc_header **fs_descriptors; |
|
const struct usb_desc_header **hs_descriptors; |
|
/* Entity 1 type is at entity_types[0] */ |
|
const entity_type_t *entity_types; |
|
/* Array of indexes to data endpoint descriptor in descriptors set. |
|
* First AudioStreaming interface is at ep_indexes[0]. Index is 0 if |
|
* the interface is external interface (Type IV), i.e. no endpoint. |
|
*/ |
|
const uint16_t *ep_indexes; |
|
/* Same as ep_indexes, but for explicit feedback endpoints. */ |
|
const uint16_t *fb_indexes; |
|
/* First AudioStreaming interface Terminal ID is at as_terminals[0]. */ |
|
const uint8_t *as_terminals; |
|
/* Number of interfaces (ep_indexes, fb_indexes and as_terminals size) */ |
|
uint8_t num_ifaces; |
|
/* Number of entities (entity_type array size) */ |
|
uint8_t num_entities; |
|
}; |
|
|
|
static entity_type_t id_type(struct usbd_class_data *const c_data, uint8_t id) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
const struct uac2_cfg *cfg = dev->config; |
|
|
|
if ((id - 1) < cfg->num_entities) { |
|
return cfg->entity_types[id - 1]; |
|
} |
|
|
|
return ENTITY_TYPE_INVALID; |
|
} |
|
|
|
static const struct usb_ep_descriptor * |
|
get_as_data_ep(struct usbd_class_data *const c_data, int as_idx) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
const struct uac2_cfg *cfg = dev->config; |
|
const struct usb_desc_header *desc = NULL; |
|
const struct usb_desc_header **descriptors; |
|
|
|
if (usbd_bus_speed(c_data->uds_ctx) == USBD_SPEED_FS) { |
|
descriptors = cfg->fs_descriptors; |
|
} else { |
|
descriptors = cfg->hs_descriptors; |
|
} |
|
|
|
if ((as_idx >= 0) && (as_idx < cfg->num_ifaces) && |
|
cfg->ep_indexes[as_idx] && descriptors) { |
|
desc = descriptors[cfg->ep_indexes[as_idx]]; |
|
} |
|
|
|
return (const struct usb_ep_descriptor *)desc; |
|
} |
|
|
|
static const struct usb_ep_descriptor * |
|
get_as_feedback_ep(struct usbd_class_data *const c_data, int as_idx) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
const struct uac2_cfg *cfg = dev->config; |
|
const struct usb_desc_header *desc = NULL; |
|
const struct usb_desc_header **descriptors; |
|
|
|
if (usbd_bus_speed(c_data->uds_ctx) == USBD_SPEED_FS) { |
|
descriptors = cfg->fs_descriptors; |
|
} else { |
|
descriptors = cfg->hs_descriptors; |
|
} |
|
|
|
if ((as_idx < cfg->num_ifaces) && cfg->fb_indexes[as_idx] && |
|
descriptors) { |
|
desc = descriptors[cfg->fb_indexes[as_idx]]; |
|
} |
|
|
|
return (const struct usb_ep_descriptor *)desc; |
|
} |
|
|
|
static int ep_to_as_interface(const struct device *dev, uint8_t ep, bool *fb) |
|
{ |
|
const struct uac2_cfg *cfg = dev->config; |
|
const struct usb_ep_descriptor *desc; |
|
|
|
for (int i = 0; i < cfg->num_ifaces; i++) { |
|
if (!cfg->ep_indexes[i]) { |
|
/* If there is no data endpoint there cannot be feedback |
|
* endpoint. Simply skip external interfaces. |
|
*/ |
|
continue; |
|
} |
|
|
|
desc = get_as_data_ep(cfg->c_data, i); |
|
if (desc && (ep == desc->bEndpointAddress)) { |
|
*fb = false; |
|
return i; |
|
} |
|
|
|
desc = get_as_feedback_ep(cfg->c_data, i); |
|
if (desc && (ep == desc->bEndpointAddress)) { |
|
*fb = true; |
|
return i; |
|
} |
|
} |
|
|
|
*fb = false; |
|
return -ENOENT; |
|
} |
|
|
|
static int terminal_to_as_interface(const struct device *dev, uint8_t terminal) |
|
{ |
|
const struct uac2_cfg *cfg = dev->config; |
|
|
|
for (int as_idx = 0; as_idx < cfg->num_ifaces; as_idx++) { |
|
if (terminal == cfg->as_terminals[as_idx]) { |
|
return as_idx; |
|
} |
|
} |
|
|
|
return -ENOENT; |
|
} |
|
|
|
void usbd_uac2_set_ops(const struct device *dev, |
|
const struct uac2_ops *ops, void *user_data) |
|
{ |
|
const struct uac2_cfg *cfg = dev->config; |
|
struct uac2_ctx *ctx = dev->data; |
|
|
|
__ASSERT(ops->sof_cb, "SOF callback is mandatory"); |
|
__ASSERT(ops->terminal_update_cb, "terminal_update_cb is mandatory"); |
|
|
|
for (uint8_t i = 0U; i < cfg->num_ifaces; i++) { |
|
const uint16_t ep_idx = cfg->ep_indexes[i]; |
|
|
|
if (cfg->fb_indexes[i] != 0U) { |
|
__ASSERT(ops->feedback_cb, "feedback_cb is mandatory"); |
|
} |
|
|
|
if (ep_idx) { |
|
const struct usb_ep_descriptor *desc = NULL; |
|
|
|
if (cfg->fs_descriptors != NULL) { |
|
/* If fs_descriptors is non-NULL and ep_idx is non-zero then |
|
* cfg->fs_descriptors[ep_idx] is non-NULL |
|
*/ |
|
desc = (const struct usb_ep_descriptor *) |
|
cfg->fs_descriptors[ep_idx]; |
|
} else if (cfg->hs_descriptors != NULL) { |
|
/* If hs_descriptors is non-NULL and ep_idx is non-zero then |
|
* cfg->hs_descriptors[ep_idx] is non-NULL |
|
*/ |
|
desc = (const struct usb_ep_descriptor *) |
|
cfg->hs_descriptors[ep_idx]; |
|
} |
|
|
|
if (desc != NULL) { |
|
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"); |
|
} |
|
} |
|
} |
|
} |
|
|
|
ctx->ops = ops; |
|
ctx->user_data = user_data; |
|
} |
|
|
|
static struct net_buf * |
|
uac2_buf_alloc(const uint8_t ep, void *data, uint16_t size) |
|
{ |
|
struct net_buf *buf = NULL; |
|
struct udc_buf_info *bi; |
|
|
|
__ASSERT(IS_UDC_ALIGNED(data), "Application provided unaligned buffer"); |
|
|
|
buf = net_buf_alloc_with_data(&uac2_pool, data, size, K_NO_WAIT); |
|
if (!buf) { |
|
return NULL; |
|
} |
|
|
|
bi = udc_get_buf_info(buf); |
|
bi->ep = ep; |
|
|
|
if (USB_EP_DIR_IS_OUT(ep)) { |
|
/* Buffer is empty, USB stack will write data from host */ |
|
buf->len = 0; |
|
} |
|
|
|
return buf; |
|
} |
|
|
|
int usbd_uac2_send(const struct device *dev, uint8_t terminal, |
|
void *data, uint16_t size) |
|
{ |
|
const struct uac2_cfg *cfg = dev->config; |
|
struct uac2_ctx *ctx = dev->data; |
|
struct net_buf *buf; |
|
const struct usb_ep_descriptor *desc; |
|
atomic_t *queued_bits = &ctx->as_queued; |
|
uint8_t ep = 0; |
|
int as_idx = terminal_to_as_interface(dev, terminal); |
|
int ret; |
|
|
|
desc = get_as_data_ep(cfg->c_data, as_idx); |
|
if (desc) { |
|
ep = desc->bEndpointAddress; |
|
} |
|
|
|
if (!ep) { |
|
LOG_ERR("No endpoint for terminal %d", terminal); |
|
return -ENOENT; |
|
} |
|
|
|
if (!atomic_test_bit(&ctx->as_active, as_idx)) { |
|
/* Host is not interested in the data */ |
|
ctx->ops->buf_release_cb(dev, terminal, data, ctx->user_data); |
|
return 0; |
|
} |
|
|
|
if (atomic_test_and_set_bit(queued_bits, as_idx)) { |
|
queued_bits = &ctx->as_double; |
|
if (atomic_test_and_set_bit(queued_bits, as_idx)) { |
|
LOG_DBG("Already double queued on 0x%02x", ep); |
|
return -EAGAIN; |
|
} |
|
} |
|
|
|
buf = uac2_buf_alloc(ep, data, size); |
|
if (!buf) { |
|
/* This shouldn't really happen because netbuf should be large |
|
* enough, but if it does all we loose is just single packet. |
|
*/ |
|
LOG_ERR("No netbuf for send"); |
|
atomic_clear_bit(queued_bits, as_idx); |
|
return -ENOMEM; |
|
} |
|
|
|
ret = usbd_ep_enqueue(cfg->c_data, buf); |
|
if (ret) { |
|
LOG_ERR("Failed to enqueue net_buf for 0x%02x", ep); |
|
net_buf_unref(buf); |
|
atomic_clear_bit(queued_bits, as_idx); |
|
} |
|
|
|
return ret; |
|
} |
|
|
|
static void schedule_iso_out_read(struct usbd_class_data *const c_data, |
|
uint8_t ep, uint16_t mps, uint8_t terminal) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
const struct uac2_cfg *cfg = dev->config; |
|
struct uac2_ctx *ctx = dev->data; |
|
struct net_buf *buf; |
|
void *data_buf; |
|
int as_idx = terminal_to_as_interface(dev, terminal); |
|
int ret; |
|
|
|
/* All calls to this function are internal to class, if terminal is not |
|
* associated with interface there is a bug in class implementation. |
|
*/ |
|
__ASSERT_NO_MSG((as_idx >= 0) && (as_idx < cfg->num_ifaces)); |
|
/* Silence warning if asserts are not enabled */ |
|
ARG_UNUSED(cfg); |
|
|
|
if (!((as_idx >= 0) && atomic_test_bit(&ctx->as_active, as_idx))) { |
|
/* Host won't send data */ |
|
return; |
|
} |
|
|
|
if (atomic_test_and_set_bit(&ctx->as_queued, as_idx)) { |
|
/* Transfer already queued - do not requeue */ |
|
return; |
|
} |
|
|
|
/* Prepare transfer to read audio OUT data from host */ |
|
data_buf = ctx->ops->get_recv_buf(dev, terminal, mps, ctx->user_data); |
|
if (!data_buf) { |
|
LOG_ERR("No data buffer for terminal %d", terminal); |
|
atomic_clear_bit(&ctx->as_queued, as_idx); |
|
return; |
|
} |
|
|
|
buf = uac2_buf_alloc(ep, data_buf, mps); |
|
if (!buf) { |
|
LOG_ERR("No netbuf for read"); |
|
/* Netbuf pool should be large enough, but if for some reason |
|
* we are out of netbuf, there's nothing better to do than to |
|
* pass the buffer back to application. |
|
*/ |
|
ctx->ops->data_recv_cb(dev, terminal, |
|
data_buf, 0, ctx->user_data); |
|
atomic_clear_bit(&ctx->as_queued, as_idx); |
|
return; |
|
} |
|
|
|
ret = usbd_ep_enqueue(c_data, buf); |
|
if (ret) { |
|
LOG_ERR("Failed to enqueue net_buf for 0x%02x", ep); |
|
net_buf_unref(buf); |
|
atomic_clear_bit(&ctx->as_queued, as_idx); |
|
} |
|
} |
|
|
|
static void write_explicit_feedback(struct usbd_class_data *const c_data, |
|
uint8_t ep, uint8_t terminal) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
struct usbd_context *uds_ctx = usbd_class_get_ctx(c_data); |
|
struct uac2_ctx *ctx = dev->data; |
|
struct net_buf *buf; |
|
struct udc_buf_info *bi; |
|
uint32_t fb_value; |
|
int as_idx = terminal_to_as_interface(dev, terminal); |
|
int ret; |
|
|
|
__ASSERT_NO_MSG(as_idx >= 0); |
|
|
|
buf = net_buf_alloc(&uac2_pool, K_NO_WAIT); |
|
if (!buf) { |
|
LOG_ERR("No buf for feedback"); |
|
return; |
|
} |
|
|
|
bi = udc_get_buf_info(buf); |
|
bi->ep = ep; |
|
|
|
fb_value = ctx->ops->feedback_cb(dev, terminal, ctx->user_data); |
|
|
|
if (usbd_bus_speed(uds_ctx) == USBD_SPEED_FS) { |
|
net_buf_add_le24(buf, fb_value); |
|
} else { |
|
net_buf_add_le32(buf, fb_value); |
|
} |
|
|
|
ret = usbd_ep_enqueue(c_data, buf); |
|
if (ret) { |
|
LOG_ERR("Failed to enqueue net_buf for 0x%02x", ep); |
|
net_buf_unref(buf); |
|
} else { |
|
ctx->fb_queued |= BIT(as_idx); |
|
} |
|
} |
|
|
|
void uac2_update(struct usbd_class_data *const c_data, |
|
uint8_t iface, uint8_t alternate) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
struct usbd_context *uds_ctx = usbd_class_get_ctx(c_data); |
|
const struct uac2_cfg *cfg = dev->config; |
|
struct uac2_ctx *ctx = dev->data; |
|
const struct usb_desc_header **descriptors; |
|
const struct usb_association_descriptor *iad; |
|
const struct usb_ep_descriptor *data_ep, *fb_ep; |
|
uint8_t as_idx; |
|
bool microframes; |
|
|
|
LOG_DBG("iface %d alt %d", iface, alternate); |
|
|
|
/* Audio class is forbidden on Low-Speed, therefore the only possibility |
|
* for not using microframes is when device operates at Full-Speed. |
|
*/ |
|
if (usbd_bus_speed(uds_ctx) == USBD_SPEED_FS) { |
|
microframes = false; |
|
descriptors = cfg->fs_descriptors; |
|
} else { |
|
microframes = true; |
|
descriptors = cfg->hs_descriptors; |
|
} |
|
|
|
if (!descriptors) { |
|
return; |
|
} |
|
|
|
iad = (const struct usb_association_descriptor *)descriptors[0]; |
|
|
|
/* AudioControl interface (bFirstInterface) doesn't have alternate |
|
* configurations, therefore the iface must be AudioStreaming. |
|
*/ |
|
__ASSERT_NO_MSG((iface > iad->bFirstInterface) && |
|
(iface < iad->bFirstInterface + iad->bInterfaceCount)); |
|
as_idx = iface - iad->bFirstInterface - 1; |
|
|
|
/* Notify application about terminal state change */ |
|
ctx->ops->terminal_update_cb(dev, cfg->as_terminals[as_idx], alternate, |
|
microframes, ctx->user_data); |
|
|
|
if (alternate == 0) { |
|
/* Mark interface as inactive, any pending endpoint transfers |
|
* were already cancelled by the USB stack. |
|
*/ |
|
atomic_clear_bit(&ctx->as_active, as_idx); |
|
return; |
|
} |
|
|
|
atomic_set_bit(&ctx->as_active, as_idx); |
|
|
|
data_ep = get_as_data_ep(c_data, as_idx); |
|
/* External interfaces (i.e. NULL data_ep) do not have alternate |
|
* configuration and therefore data_ep must be valid here. |
|
*/ |
|
__ASSERT_NO_MSG(data_ep); |
|
|
|
if (USB_EP_DIR_IS_OUT(data_ep->bEndpointAddress)) { |
|
schedule_iso_out_read(c_data, data_ep->bEndpointAddress, |
|
sys_le16_to_cpu(data_ep->wMaxPacketSize), |
|
cfg->as_terminals[as_idx]); |
|
|
|
fb_ep = get_as_feedback_ep(c_data, as_idx); |
|
if (fb_ep) { |
|
write_explicit_feedback(c_data, fb_ep->bEndpointAddress, |
|
cfg->as_terminals[as_idx]); |
|
} |
|
} |
|
} |
|
|
|
/* 5.2.2 Control Request Layout: "As a general rule, when an attribute value |
|
* is set, a Control will automatically adjust the passed value to the closest |
|
* available valid value." |
|
* |
|
* The values array must be sorted ascending with at least 1 element. |
|
*/ |
|
static uint32_t find_closest(const uint32_t input, const uint32_t *values, |
|
const size_t values_count) |
|
{ |
|
size_t i; |
|
|
|
__ASSERT_NO_MSG(values_count); |
|
|
|
for (i = 0; i < values_count; i++) { |
|
if (input == values[i]) { |
|
/* Exact match */ |
|
return input; |
|
} else if (input < values[i]) { |
|
break; |
|
} |
|
} |
|
|
|
if (i == values_count) { |
|
/* All values are smaller than input, return largest value */ |
|
return values[i - 1]; |
|
} |
|
|
|
if (i == 0) { |
|
/* All values are larger than input, return smallest value */ |
|
return values[i]; |
|
} |
|
|
|
/* At this point values[i] is larger than input and values[i - 1] is |
|
* smaller than input, find and return the one that is closer, favoring |
|
* bigger value if input is exactly in the middle between the two. |
|
*/ |
|
if ((values[i] - input) > (input - values[i - 1])) { |
|
return values[i - 1]; |
|
} else { |
|
return values[i]; |
|
} |
|
} |
|
|
|
/* Table 5-6: 4-byte Control CUR Parameter Block */ |
|
static void layout3_cur_response(struct net_buf *const buf, uint16_t length, |
|
const uint32_t value) |
|
{ |
|
uint8_t tmp[4]; |
|
|
|
/* dCUR */ |
|
sys_put_le32(value, tmp); |
|
net_buf_add_mem(buf, tmp, MIN(length, 4)); |
|
} |
|
|
|
static int layout3_cur_request(const struct net_buf *const buf, uint32_t *out) |
|
{ |
|
uint8_t tmp[4]; |
|
|
|
if (buf->len != 4) { |
|
return -EINVAL; |
|
} |
|
|
|
memcpy(tmp, buf->data, sizeof(tmp)); |
|
*out = sys_get_le32(tmp); |
|
return 0; |
|
} |
|
|
|
/* Table 5-7: 4-byte Control RANGE Parameter Block */ |
|
static void layout3_range_response(struct net_buf *const buf, uint16_t length, |
|
const uint32_t *min, const uint32_t *max, |
|
const uint32_t *res, int n) |
|
{ |
|
uint16_t to_add; |
|
uint8_t tmp[4]; |
|
int i; |
|
int item; |
|
|
|
/* wNumSubRanges */ |
|
sys_put_le16(n, tmp); |
|
to_add = MIN(length, 2); |
|
net_buf_add_mem(buf, tmp, to_add); |
|
length -= to_add; |
|
|
|
/* Keep adding dMIN, dMAX, dRES as long as we have entries to add and |
|
* we didn't reach wLength response limit. |
|
*/ |
|
i = item = 0; |
|
while ((length > 0) && (i < n)) { |
|
to_add = MIN(length, 4); |
|
if (item == 0) { |
|
sys_put_le32(min[i], tmp); |
|
} else if (item == 1) { |
|
sys_put_le32(max[i], tmp); |
|
} else if (item == 2) { |
|
if (res) { |
|
sys_put_le32(res[i], tmp); |
|
} else { |
|
memset(tmp, 0, 4); |
|
} |
|
} |
|
net_buf_add_mem(buf, tmp, to_add); |
|
length -= to_add; |
|
|
|
if (++item == 3) { |
|
item = 0; |
|
i++; |
|
} |
|
} |
|
} |
|
|
|
static int get_clock_source_request(struct usbd_class_data *const c_data, |
|
const struct usb_setup_packet *const setup, |
|
struct net_buf *const buf) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
struct uac2_ctx *ctx = dev->data; |
|
const uint32_t *frequencies; |
|
const uint32_t clock_id = CONTROL_ENTITY_ID(setup); |
|
size_t count; |
|
|
|
/* Channel Number must be zero */ |
|
if (CONTROL_CHANNEL_NUMBER(setup) != 0) { |
|
LOG_DBG("Clock source control with channel %d", |
|
CONTROL_CHANNEL_NUMBER(setup)); |
|
errno = -EINVAL; |
|
return 0; |
|
} |
|
|
|
count = clock_frequencies(c_data, clock_id, &frequencies); |
|
|
|
if (CONTROL_SELECTOR(setup) == CS_SAM_FREQ_CONTROL) { |
|
if (CONTROL_ATTRIBUTE(setup) == CUR) { |
|
if (count == 1) { |
|
layout3_cur_response(buf, setup->wLength, |
|
frequencies[0]); |
|
return 0; |
|
} |
|
|
|
if (ctx->ops->get_sample_rate) { |
|
uint32_t hz; |
|
|
|
hz = ctx->ops->get_sample_rate(dev, clock_id, |
|
ctx->user_data); |
|
layout3_cur_response(buf, setup->wLength, hz); |
|
return 0; |
|
} |
|
} else if (CONTROL_ATTRIBUTE(setup) == RANGE) { |
|
layout3_range_response(buf, setup->wLength, frequencies, |
|
frequencies, NULL, count); |
|
return 0; |
|
} |
|
} else { |
|
LOG_DBG("Unhandled clock control selector 0x%02x", |
|
CONTROL_SELECTOR(setup)); |
|
} |
|
|
|
errno = -ENOTSUP; |
|
return 0; |
|
} |
|
|
|
static int set_clock_source_request(struct usbd_class_data *const c_data, |
|
const struct usb_setup_packet *const setup, |
|
const struct net_buf *const buf) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
struct uac2_ctx *ctx = dev->data; |
|
const uint32_t *frequencies; |
|
const uint32_t clock_id = CONTROL_ENTITY_ID(setup); |
|
size_t count; |
|
|
|
/* Channel Number must be zero */ |
|
if (CONTROL_CHANNEL_NUMBER(setup) != 0) { |
|
LOG_DBG("Clock source control with channel %d", |
|
CONTROL_CHANNEL_NUMBER(setup)); |
|
errno = -EINVAL; |
|
return 0; |
|
} |
|
|
|
count = clock_frequencies(c_data, clock_id, &frequencies); |
|
|
|
if (CONTROL_SELECTOR(setup) == CS_SAM_FREQ_CONTROL) { |
|
if (CONTROL_ATTRIBUTE(setup) == CUR) { |
|
uint32_t requested, hz; |
|
int err; |
|
|
|
err = layout3_cur_request(buf, &requested); |
|
if (err) { |
|
errno = err; |
|
return 0; |
|
} |
|
|
|
hz = find_closest(requested, frequencies, count); |
|
|
|
if (ctx->ops->set_sample_rate == NULL) { |
|
/* The set_sample_rate() callback is optional |
|
* if there is only one supported sample rate. |
|
*/ |
|
if (count > 1) { |
|
errno = -ENOTSUP; |
|
} |
|
return 0; |
|
} |
|
|
|
err = ctx->ops->set_sample_rate(dev, clock_id, hz, |
|
ctx->user_data); |
|
if (err) { |
|
errno = err; |
|
} |
|
|
|
return 0; |
|
} |
|
} else { |
|
LOG_DBG("Unhandled clock control selector 0x%02x", |
|
CONTROL_SELECTOR(setup)); |
|
} |
|
|
|
errno = -ENOTSUP; |
|
return 0; |
|
} |
|
|
|
static int uac2_control_to_dev(struct usbd_class_data *const c_data, |
|
const struct usb_setup_packet *const setup, |
|
const struct net_buf *const buf) |
|
{ |
|
entity_type_t entity_type; |
|
|
|
if (CONTROL_ATTRIBUTE(setup) != CUR) { |
|
errno = -ENOTSUP; |
|
return 0; |
|
} |
|
|
|
if (setup->bmRequestType == SET_CLASS_REQUEST_TYPE) { |
|
entity_type = id_type(c_data, CONTROL_ENTITY_ID(setup)); |
|
if (entity_type == ENTITY_TYPE_CLOCK_SOURCE) { |
|
return set_clock_source_request(c_data, setup, buf); |
|
} |
|
} |
|
|
|
errno = -ENOTSUP; |
|
return 0; |
|
} |
|
|
|
static int uac2_control_to_host(struct usbd_class_data *const c_data, |
|
const struct usb_setup_packet *const setup, |
|
struct net_buf *const buf) |
|
{ |
|
entity_type_t entity_type; |
|
|
|
if ((CONTROL_ATTRIBUTE(setup) != CUR) && |
|
(CONTROL_ATTRIBUTE(setup) != RANGE)) { |
|
errno = -ENOTSUP; |
|
return 0; |
|
} |
|
|
|
if (setup->bmRequestType == GET_CLASS_REQUEST_TYPE) { |
|
entity_type = id_type(c_data, CONTROL_ENTITY_ID(setup)); |
|
if (entity_type == ENTITY_TYPE_CLOCK_SOURCE) { |
|
return get_clock_source_request(c_data, setup, buf); |
|
} |
|
} |
|
|
|
errno = -ENOTSUP; |
|
return 0; |
|
} |
|
|
|
static int uac2_request(struct usbd_class_data *const c_data, struct net_buf *buf, |
|
int err) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
const struct uac2_cfg *cfg = dev->config; |
|
struct uac2_ctx *ctx = dev->data; |
|
struct usbd_context *uds_ctx = usbd_class_get_ctx(c_data); |
|
struct udc_buf_info *bi; |
|
uint8_t ep, terminal; |
|
uint16_t mps; |
|
int as_idx; |
|
bool is_feedback; |
|
|
|
bi = udc_get_buf_info(buf); |
|
if (err) { |
|
if (err == -ECONNABORTED) { |
|
LOG_WRN("request ep 0x%02x, len %u cancelled", |
|
bi->ep, buf->len); |
|
} else { |
|
LOG_ERR("request ep 0x%02x, len %u failed", |
|
bi->ep, buf->len); |
|
} |
|
} |
|
|
|
mps = buf->size; |
|
ep = bi->ep; |
|
as_idx = ep_to_as_interface(dev, ep, &is_feedback); |
|
__ASSERT_NO_MSG((as_idx >= 0) && (as_idx < cfg->num_ifaces)); |
|
terminal = cfg->as_terminals[as_idx]; |
|
|
|
if (is_feedback) { |
|
ctx->fb_queued &= ~BIT(as_idx); |
|
} else if (!atomic_test_and_clear_bit(&ctx->as_queued, as_idx) || buf->frags) { |
|
atomic_clear_bit(&ctx->as_double, as_idx); |
|
} |
|
|
|
if (USB_EP_DIR_IS_OUT(ep)) { |
|
ctx->ops->data_recv_cb(dev, terminal, buf->__buf, buf->len, |
|
ctx->user_data); |
|
} else if (!is_feedback) { |
|
ctx->ops->buf_release_cb(dev, terminal, buf->__buf, ctx->user_data); |
|
if (buf->frags) { |
|
ctx->ops->buf_release_cb(dev, terminal, buf->frags->__buf, ctx->user_data); |
|
} |
|
} |
|
|
|
usbd_ep_buf_free(uds_ctx, buf); |
|
if (err) { |
|
return 0; |
|
} |
|
|
|
/* Reschedule the read or explicit feedback write */ |
|
if (USB_EP_DIR_IS_OUT(ep)) { |
|
schedule_iso_out_read(c_data, ep, mps, terminal); |
|
} else if (is_feedback) { |
|
write_explicit_feedback(c_data, ep, cfg->as_terminals[as_idx]); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static void uac2_sof(struct usbd_class_data *const c_data) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
const struct usb_ep_descriptor *data_ep; |
|
const struct usb_ep_descriptor *feedback_ep; |
|
const struct uac2_cfg *cfg = dev->config; |
|
struct uac2_ctx *ctx = dev->data; |
|
int as_idx; |
|
|
|
ctx->ops->sof_cb(dev, ctx->user_data); |
|
|
|
for (as_idx = 0; as_idx < cfg->num_ifaces; as_idx++) { |
|
/* Make sure OUT endpoint has read request pending. The request |
|
* won't be pending only if there was buffer underrun, i.e. the |
|
* application failed to supply receive buffer. |
|
*/ |
|
data_ep = get_as_data_ep(c_data, as_idx); |
|
if (data_ep && USB_EP_DIR_IS_OUT(data_ep->bEndpointAddress)) { |
|
schedule_iso_out_read(c_data, data_ep->bEndpointAddress, |
|
sys_le16_to_cpu(data_ep->wMaxPacketSize), |
|
cfg->as_terminals[as_idx]); |
|
} |
|
|
|
/* Skip interfaces without explicit feedback endpoint */ |
|
feedback_ep = get_as_feedback_ep(c_data, as_idx); |
|
if (feedback_ep == NULL) { |
|
continue; |
|
} |
|
|
|
/* We didn't get feedback write request callback yet, skip it |
|
* for now to allow faster recovery (i.e. reduce workload to be |
|
* done during this frame). |
|
*/ |
|
if (ctx->fb_queued & BIT(as_idx)) { |
|
continue; |
|
} |
|
|
|
/* Only send feedback if host has enabled alternate interface */ |
|
if (!atomic_test_bit(&ctx->as_active, as_idx)) { |
|
continue; |
|
} |
|
|
|
/* Make feedback available on every frame (value "sent" in |
|
* previous SOF is "gone" even if USB host did not attempt to |
|
* read it). |
|
*/ |
|
write_explicit_feedback(c_data, feedback_ep->bEndpointAddress, |
|
cfg->as_terminals[as_idx]); |
|
} |
|
} |
|
|
|
static void *uac2_get_desc(struct usbd_class_data *const c_data, |
|
const enum usbd_speed speed) |
|
{ |
|
struct device *dev = usbd_class_get_private(c_data); |
|
const struct uac2_cfg *cfg = dev->config; |
|
|
|
if (USBD_SUPPORTS_HIGH_SPEED && speed == USBD_SPEED_HS) { |
|
return cfg->hs_descriptors; |
|
} |
|
|
|
return cfg->fs_descriptors; |
|
} |
|
|
|
static int uac2_init(struct usbd_class_data *const c_data) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
struct uac2_ctx *ctx = dev->data; |
|
|
|
if (ctx->ops == NULL) { |
|
LOG_ERR("Application did not register UAC2 ops"); |
|
return -EINVAL; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
struct usbd_class_api uac2_api = { |
|
.update = uac2_update, |
|
.control_to_dev = uac2_control_to_dev, |
|
.control_to_host = uac2_control_to_host, |
|
.request = uac2_request, |
|
.sof = uac2_sof, |
|
.get_desc = uac2_get_desc, |
|
.init = uac2_init, |
|
}; |
|
|
|
#define DEFINE_ENTITY_TYPES(node) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_clock_source), ( \ |
|
ENTITY_TYPE_CLOCK_SOURCE \ |
|
)) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_input_terminal), ( \ |
|
ENTITY_TYPE_INPUT_TERMINAL \ |
|
)) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_output_terminal), ( \ |
|
ENTITY_TYPE_OUTPUT_TERMINAL \ |
|
)) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_audio_streaming), ( \ |
|
ENTITY_TYPE_INVALID \ |
|
)) \ |
|
, /* Comma here causes unknown types to fail at compile time */ |
|
#define DEFINE_AS_EP_INDEXES(node) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_audio_streaming), ( \ |
|
COND_CODE_1(AS_HAS_ISOCHRONOUS_DATA_ENDPOINT(node), \ |
|
(UAC2_DESCRIPTOR_AS_DATA_EP_INDEX(node),), (0,)) \ |
|
)) |
|
#define DEFINE_AS_FB_INDEXES(node) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_audio_streaming), ( \ |
|
COND_CODE_1(AS_HAS_EXPLICIT_FEEDBACK_ENDPOINT(node), \ |
|
(UAC2_DESCRIPTOR_AS_FEEDBACK_EP_INDEX(node),), (0,)) \ |
|
)) |
|
#define DEFINE_AS_TERMINALS(node) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_audio_streaming), ( \ |
|
ENTITY_ID(DT_PROP(node, linked_terminal)), \ |
|
)) |
|
|
|
#define FREQUENCY_TABLE_NAME(node, i) \ |
|
UTIL_CAT(frequencies_##i##_, ENTITY_ID(node)) |
|
#define DEFINE_CLOCK_SOURCES(node, i) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_clock_source), ( \ |
|
static const uint32_t FREQUENCY_TABLE_NAME(node, i)[] = \ |
|
DT_PROP(node, sampling_frequencies); \ |
|
)) |
|
|
|
#define DEFINE_LOOKUP_TABLES(i) \ |
|
static const entity_type_t entity_types_##i[] = { \ |
|
DT_INST_FOREACH_CHILD_STATUS_OKAY(i, DEFINE_ENTITY_TYPES) \ |
|
}; \ |
|
static const uint16_t ep_indexes_##i[] = { \ |
|
DT_INST_FOREACH_CHILD_STATUS_OKAY(i, DEFINE_AS_EP_INDEXES) \ |
|
}; \ |
|
static const uint16_t fb_indexes_##i[] = { \ |
|
DT_INST_FOREACH_CHILD_STATUS_OKAY(i, DEFINE_AS_FB_INDEXES) \ |
|
}; \ |
|
static const uint8_t as_terminals_##i[] = { \ |
|
DT_INST_FOREACH_CHILD_STATUS_OKAY(i, DEFINE_AS_TERMINALS) \ |
|
}; \ |
|
DT_INST_FOREACH_CHILD_STATUS_OKAY_VARGS(i, DEFINE_CLOCK_SOURCES, i) |
|
|
|
#define DEFINE_UAC2_CLASS_DATA(inst) \ |
|
VALIDATE_INSTANCE(DT_DRV_INST(inst)) \ |
|
static struct uac2_ctx uac2_ctx_##inst; \ |
|
UAC2_DESCRIPTOR_ARRAYS(DT_DRV_INST(inst)) \ |
|
IF_ENABLED(UAC2_ALLOWED_AT_FULL_SPEED(DT_DRV_INST(inst)), ( \ |
|
static const struct usb_desc_header *uac2_fs_desc_##inst[] = \ |
|
UAC2_FS_DESCRIPTOR_PTRS_ARRAY(DT_DRV_INST(inst)); \ |
|
)) \ |
|
IF_ENABLED(UAC2_ALLOWED_AT_HIGH_SPEED(DT_DRV_INST(inst)), ( \ |
|
static const struct usb_desc_header *uac2_hs_desc_##inst[] = \ |
|
UAC2_HS_DESCRIPTOR_PTRS_ARRAY(DT_DRV_INST(inst)); \ |
|
)) \ |
|
USBD_DEFINE_CLASS(uac2_##inst, &uac2_api, \ |
|
(void *)DEVICE_DT_GET(DT_DRV_INST(inst)), NULL); \ |
|
DEFINE_LOOKUP_TABLES(inst) \ |
|
static const struct uac2_cfg uac2_cfg_##inst = { \ |
|
.c_data = &uac2_##inst, \ |
|
COND_CODE_1(UAC2_ALLOWED_AT_FULL_SPEED(DT_DRV_INST(inst)), \ |
|
(.fs_descriptors = uac2_fs_desc_##inst,), \ |
|
(.fs_descriptors = NULL,) \ |
|
) \ |
|
COND_CODE_1(UAC2_ALLOWED_AT_HIGH_SPEED(DT_DRV_INST(inst)), \ |
|
(.hs_descriptors = uac2_hs_desc_##inst,), \ |
|
(.hs_descriptors = NULL,) \ |
|
) \ |
|
.entity_types = entity_types_##inst, \ |
|
.ep_indexes = ep_indexes_##inst, \ |
|
.fb_indexes = fb_indexes_##inst, \ |
|
.as_terminals = as_terminals_##inst, \ |
|
.num_ifaces = ARRAY_SIZE(ep_indexes_##inst), \ |
|
.num_entities = ARRAY_SIZE(entity_types_##inst), \ |
|
}; \ |
|
BUILD_ASSERT(ARRAY_SIZE(ep_indexes_##inst) <= 32, \ |
|
"UAC2 implementation supports up to 32 AS interfaces"); \ |
|
BUILD_ASSERT(ARRAY_SIZE(entity_types_##inst) <= 255, \ |
|
"UAC2 supports up to 255 entities"); \ |
|
DEVICE_DT_DEFINE(DT_DRV_INST(inst), NULL, NULL, \ |
|
&uac2_ctx_##inst, &uac2_cfg_##inst, \ |
|
POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE, \ |
|
NULL); |
|
DT_INST_FOREACH_STATUS_OKAY(DEFINE_UAC2_CLASS_DATA) |
|
|
|
static size_t clock_frequencies(struct usbd_class_data *const c_data, |
|
const uint8_t id, const uint32_t **frequencies) |
|
{ |
|
const struct device *dev = usbd_class_get_private(c_data); |
|
size_t count; |
|
|
|
#define GET_FREQUENCY_TABLE(node, i) \ |
|
IF_ENABLED(DT_NODE_HAS_COMPAT(node, zephyr_uac2_clock_source), ( \ |
|
} else if (id == ENTITY_ID(node)) { \ |
|
*frequencies = FREQUENCY_TABLE_NAME(node, i); \ |
|
count = ARRAY_SIZE(FREQUENCY_TABLE_NAME(node, i)); \ |
|
)) |
|
|
|
if (0) { |
|
#define SELECT_FREQUENCY_TABLE(i) \ |
|
} else if (dev == DEVICE_DT_GET(DT_DRV_INST(i))) { \ |
|
if (0) { \ |
|
DT_INST_FOREACH_CHILD_VARGS(i, GET_FREQUENCY_TABLE, i) \ |
|
} else { \ |
|
*frequencies = NULL; \ |
|
count = 0; \ |
|
} |
|
DT_INST_FOREACH_STATUS_OKAY(SELECT_FREQUENCY_TABLE) |
|
} else { |
|
*frequencies = NULL; |
|
count = 0; |
|
} |
|
|
|
return count; |
|
}
|
|
|