| /* |
| * 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); |
| |
| 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; |
| } |