354 lines
7.5 KiB
C
354 lines
7.5 KiB
C
/*
|
|
* 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;
|
|
}
|