zephyr/subsys/usb/device/usb_transfer.c
Tomasz Moń 08bd1c5ec2 usb: device: Fix ZLP write race condition
ZLP packet has to be read and acknowledged by host just like any other
DATA packet. Do not end transfer until the host actually acknowledged
the trailing ZLP. This fixes the race condition between host and Zephyr
application where the next transfer could be lost if host did not issue
IN token (that would read read ZLP) before the application tried to
start new transfer.

Signed-off-by: Tomasz Moń <tomasz.mon@nordicsemi.no>
2023-10-26 13:52:31 +02:00

342 lines
7.3 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);
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;
}