mirror of
https://github.com/zephyrproject-rtos/zephyr
synced 2025-08-05 11:07:51 +00:00
The USB infrastructure currently uses the system work queue for offloading transfers, CDC-ACM UART transmission/reception, and device firmware activities. This causes problems when the system work queue is also used to initiate some activities (such as UART) that normally complete without requiring an external thread: in that case the USB infrastructure is prevented from making progress because the system work queue is blocked waiting for the USB infrastructure to provide data. Break the dependency by allowing the USB infrastructure to use a dedicated work queue which doesn't depend on availability of the system work queue. Signed-off-by: Peter Bigot <peter.bigot@nordicsemi.no>
332 lines
7.0 KiB
C
332 lines
7.0 KiB
C
/*
|
|
* Copyright (c) 2018 Linaro
|
|
* Copyright (c) 2019 PHYTEC Messtechnik GmbH
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr.h>
|
|
#include <usb/usb_device.h>
|
|
#include <logging/log.h>
|
|
|
|
#include "usb_transfer.h"
|
|
#include "usb_work_q.h"
|
|
|
|
LOG_MODULE_REGISTER(usb_transfer, CONFIG_USB_DEVICE_LOG_LEVEL);
|
|
|
|
struct usb_transfer_sync_priv {
|
|
int tsize;
|
|
struct k_sem sem;
|
|
};
|
|
|
|
struct usb_transfer_data {
|
|
/** endpoint associated to the transfer */
|
|
uint8_t ep;
|
|
/** Transfer status */
|
|
int status;
|
|
/** Transfer read/write buffer */
|
|
uint8_t *buffer;
|
|
/** Transfer buffer size */
|
|
size_t bsize;
|
|
/** Transferred size */
|
|
size_t tsize;
|
|
/** Transfer callback */
|
|
usb_transfer_callback cb;
|
|
/** Transfer caller private data */
|
|
void *priv;
|
|
/** Transfer synchronization semaphore */
|
|
struct k_sem sem;
|
|
/** Transfer read/write work */
|
|
struct k_work work;
|
|
/** Transfer flags */
|
|
unsigned int flags;
|
|
};
|
|
|
|
/** Max number of parallel transfers */
|
|
static struct usb_transfer_data ut_data[CONFIG_USB_MAX_NUM_TRANSFERS];
|
|
|
|
/* Transfer management */
|
|
static struct usb_transfer_data *usb_ep_get_transfer(uint8_t ep)
|
|
{
|
|
for (int i = 0; i < ARRAY_SIZE(ut_data); i++) {
|
|
if (ut_data[i].ep == ep) {
|
|
return &ut_data[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
bool usb_transfer_is_busy(uint8_t ep)
|
|
{
|
|
struct usb_transfer_data *trans = usb_ep_get_transfer(ep);
|
|
|
|
if (trans && trans->status == -EBUSY) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void usb_transfer_work(struct k_work *item)
|
|
{
|
|
struct usb_transfer_data *trans;
|
|
int ret = 0;
|
|
uint32_t bytes;
|
|
uint8_t ep;
|
|
|
|
trans = CONTAINER_OF(item, struct usb_transfer_data, work);
|
|
ep = trans->ep;
|
|
|
|
if (trans->status != -EBUSY) {
|
|
/* transfer cancelled or already completed */
|
|
LOG_DBG("Transfer cancelled or completed, ep 0x%02x", ep);
|
|
goto done;
|
|
}
|
|
|
|
if (trans->flags & USB_TRANS_WRITE) {
|
|
if (!trans->bsize) {
|
|
if (!(trans->flags & USB_TRANS_NO_ZLP)) {
|
|
LOG_DBG("Transfer ZLP");
|
|
usb_write(ep, NULL, 0, NULL);
|
|
}
|
|
trans->status = 0;
|
|
goto done;
|
|
}
|
|
|
|
ret = usb_write(ep, trans->buffer, trans->bsize, &bytes);
|
|
if (ret) {
|
|
LOG_ERR("Transfer error %d, ep 0x%02x", ret, ep);
|
|
/* transfer error */
|
|
trans->status = -EINVAL;
|
|
goto done;
|
|
}
|
|
|
|
trans->buffer += bytes;
|
|
trans->bsize -= bytes;
|
|
trans->tsize += bytes;
|
|
} else {
|
|
ret = usb_dc_ep_read_wait(ep, trans->buffer, trans->bsize,
|
|
&bytes);
|
|
if (ret) {
|
|
LOG_ERR("Transfer error %d, ep 0x%02x", ret, ep);
|
|
/* transfer error */
|
|
trans->status = -EINVAL;
|
|
goto done;
|
|
}
|
|
|
|
trans->buffer += bytes;
|
|
trans->bsize -= bytes;
|
|
trans->tsize += bytes;
|
|
|
|
/* ZLP, short-pkt or buffer full */
|
|
if (!bytes || (bytes % usb_dc_ep_mps(ep)) || !trans->bsize) {
|
|
/* transfer complete */
|
|
trans->status = 0;
|
|
goto done;
|
|
}
|
|
|
|
/* we expect mote data, clear NAK */
|
|
usb_dc_ep_read_continue(ep);
|
|
}
|
|
|
|
done:
|
|
if (trans->status != -EBUSY && trans->cb) { /* Transfer complete */
|
|
usb_transfer_callback cb = trans->cb;
|
|
int tsize = trans->tsize;
|
|
void *priv = trans->priv;
|
|
|
|
if (k_is_in_isr()) {
|
|
/* reschedule completion in thread context */
|
|
k_work_submit_to_queue(&USB_WORK_Q, &trans->work);
|
|
return;
|
|
}
|
|
|
|
LOG_DBG("Transfer done, ep 0x%02x, status %d, size %zu",
|
|
trans->ep, trans->status, trans->tsize);
|
|
|
|
trans->cb = NULL;
|
|
k_sem_give(&trans->sem);
|
|
|
|
/* Transfer completion callback */
|
|
if (trans->status != -ECANCELED) {
|
|
cb(ep, tsize, priv);
|
|
}
|
|
}
|
|
}
|
|
|
|
void usb_transfer_ep_callback(uint8_t ep, enum usb_dc_ep_cb_status_code status)
|
|
{
|
|
struct usb_transfer_data *trans = usb_ep_get_transfer(ep);
|
|
|
|
if (status != USB_DC_EP_DATA_IN && status != USB_DC_EP_DATA_OUT) {
|
|
return;
|
|
}
|
|
|
|
if (!trans) {
|
|
if (status == USB_DC_EP_DATA_OUT) {
|
|
uint32_t bytes;
|
|
/* In the unlikely case we receive data while no
|
|
* transfer is ongoing, we have to consume the data
|
|
* anyway. This is to prevent stucking reception on
|
|
* other endpoints (e.g dw driver has only one rx-fifo,
|
|
* so drain it).
|
|
*/
|
|
do {
|
|
uint8_t data;
|
|
|
|
usb_dc_ep_read_wait(ep, &data, 1, &bytes);
|
|
} while (bytes);
|
|
|
|
LOG_ERR("RX data lost, no transfer");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!k_is_in_isr() || (status == USB_DC_EP_DATA_OUT)) {
|
|
/* If we are not in IRQ context, no need to defer work */
|
|
/* Read (out) needs to be done from ep_callback */
|
|
usb_transfer_work(&trans->work);
|
|
} else {
|
|
k_work_submit_to_queue(&USB_WORK_Q, &trans->work);
|
|
}
|
|
}
|
|
|
|
int usb_transfer(uint8_t ep, uint8_t *data, size_t dlen, unsigned int flags,
|
|
usb_transfer_callback cb, void *cb_data)
|
|
{
|
|
struct usb_transfer_data *trans = NULL;
|
|
int i, key, ret = 0;
|
|
|
|
LOG_DBG("Transfer start, ep 0x%02x, data %p, dlen %zd",
|
|
ep, data, dlen);
|
|
|
|
key = irq_lock();
|
|
|
|
for (i = 0; i < ARRAY_SIZE(ut_data); i++) {
|
|
if (!k_sem_take(&ut_data[i].sem, K_NO_WAIT)) {
|
|
trans = &ut_data[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!trans) {
|
|
LOG_ERR("No transfer slot available");
|
|
ret = -ENOMEM;
|
|
goto done;
|
|
}
|
|
|
|
if (trans->status == -EBUSY) {
|
|
/* A transfer is already ongoing and not completed */
|
|
LOG_ERR("A transfer is already ongoing, ep 0x%02x", ep);
|
|
k_sem_give(&trans->sem);
|
|
ret = -EBUSY;
|
|
goto done;
|
|
}
|
|
|
|
/* Configure new transfer */
|
|
trans->ep = ep;
|
|
trans->buffer = data;
|
|
trans->bsize = dlen;
|
|
trans->tsize = 0;
|
|
trans->cb = cb;
|
|
trans->flags = flags;
|
|
trans->priv = cb_data;
|
|
trans->status = -EBUSY;
|
|
|
|
if (usb_dc_ep_mps(ep) && (dlen % usb_dc_ep_mps(ep))) {
|
|
/* no need to send ZLP since last packet will be a short one */
|
|
trans->flags |= USB_TRANS_NO_ZLP;
|
|
}
|
|
|
|
if (flags & USB_TRANS_WRITE) {
|
|
/* start writing first chunk */
|
|
k_work_submit_to_queue(&USB_WORK_Q, &trans->work);
|
|
} else {
|
|
/* ready to read, clear NAK */
|
|
ret = usb_dc_ep_read_continue(ep);
|
|
}
|
|
|
|
done:
|
|
irq_unlock(key);
|
|
return ret;
|
|
}
|
|
|
|
void usb_cancel_transfer(uint8_t ep)
|
|
{
|
|
struct usb_transfer_data *trans;
|
|
unsigned int key;
|
|
|
|
key = irq_lock();
|
|
|
|
trans = usb_ep_get_transfer(ep);
|
|
if (!trans) {
|
|
goto done;
|
|
}
|
|
|
|
if (trans->status != -EBUSY) {
|
|
goto done;
|
|
}
|
|
|
|
trans->status = -ECANCELED;
|
|
k_work_submit_to_queue(&USB_WORK_Q, &trans->work);
|
|
|
|
done:
|
|
irq_unlock(key);
|
|
}
|
|
|
|
void usb_cancel_transfers(void)
|
|
{
|
|
for (int i = 0; i < ARRAY_SIZE(ut_data); i++) {
|
|
struct usb_transfer_data *trans = &ut_data[i];
|
|
unsigned int key;
|
|
|
|
key = irq_lock();
|
|
|
|
if (trans->status == -EBUSY) {
|
|
trans->status = -ECANCELED;
|
|
k_work_submit_to_queue(&USB_WORK_Q, &trans->work);
|
|
LOG_DBG("Cancel transfer for ep: 0x%02x", trans->ep);
|
|
}
|
|
|
|
irq_unlock(key);
|
|
}
|
|
}
|
|
|
|
static void usb_transfer_sync_cb(uint8_t ep, int size, void *priv)
|
|
{
|
|
struct usb_transfer_sync_priv *pdata = priv;
|
|
|
|
pdata->tsize = size;
|
|
k_sem_give(&pdata->sem);
|
|
}
|
|
|
|
int usb_transfer_sync(uint8_t ep, uint8_t *data, size_t dlen, unsigned int flags)
|
|
{
|
|
struct usb_transfer_sync_priv pdata;
|
|
int ret;
|
|
|
|
k_sem_init(&pdata.sem, 0, 1);
|
|
|
|
ret = usb_transfer(ep, data, dlen, flags, usb_transfer_sync_cb, &pdata);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
|
|
/* Semaphore will be released by the transfer completion callback */
|
|
k_sem_take(&pdata.sem, K_FOREVER);
|
|
|
|
return pdata.tsize;
|
|
}
|
|
|
|
/* Init transfer slots */
|
|
int usb_transfer_init(void)
|
|
{
|
|
for (int i = 0; i < ARRAY_SIZE(ut_data); i++) {
|
|
k_work_init(&ut_data[i].work, usb_transfer_work);
|
|
k_sem_init(&ut_data[i].sem, 1, 1);
|
|
}
|
|
|
|
return 0;
|
|
}
|