blob: ad47c1a31291419a36cfaae9560ad4652fb23c1f [file] [log] [blame]
/*
* Copyright (c) 2018 Linaro
* Copyright (c) 2019 PHYTEC Messtechnik GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <zephyr/usb/usb_device.h>
#include <zephyr/logging/log.h>
#include "usb_transfer.h"
#include "usb_work_q.h"
LOG_MODULE_REGISTER(usb_transfer, CONFIG_USB_DEVICE_LOG_LEVEL);
#define USB_TRANSFER_SYNC_TIMEOUT 100
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 && ut_data[i].status != 0) {
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;
/* Parallel transfer to same endpoint is not supported. */
if (usb_transfer_is_busy(ep)) {
return -EBUSY;
}
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
* which might not be called when transfer was cancelled
*/
while (1) {
struct usb_transfer_data *trans;
ret = k_sem_take(&pdata.sem, K_MSEC(USB_TRANSFER_SYNC_TIMEOUT));
if (ret == 0) {
break;
}
trans = usb_ep_get_transfer(ep);
if (!trans || trans->status != -EBUSY) {
LOG_WRN("Sync transfer cancelled, ep 0x%02x", ep);
return -ECANCELED;
}
}
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;
}