/* * Copyright (c) 2018 Linaro * Copyright (c) 2019 PHYTEC Messtechnik GmbH * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #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 (size_t 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) { trans->status = 0; goto done; } /* Host have to read the ZLP just like any other DATA * packet. Set USB_TRANS_NO_ZLP flag so the transfer * will end next time we get ACK from host. */ LOG_DBG("Transfer ZLP"); trans->flags |= USB_TRANS_NO_ZLP; } 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) { /* 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 (cb) { 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 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 (size_t 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 (size_t 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 (size_t 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; }