usb: Add transfer management API

The transfer API provides 'high' level functions to manage sending and
reception of USB data. A USB (class) driver has to register the generic
usb_transfer_ep_callback as endpoint status callback in order to use
the API.

With this API, the class driver does not need to take care of low-level
usb transfer management (packet splitting, ZLP, synchronization...).
The usb_transfer methods will split transfer into multiple transactions
depending endpoint max size and controller capabilities.

Once the transfer is completed, class driver is notified by a callback.
The usb_transfer method can be executed in IRQ/atomic context.
A usb_transfer synchronous helper exists which block-waits until
transfer completion.

In write case, a transfer is complete when all data has been sent.
In read case, a transfer is complete when the exact amount of data
requested has been received or if a short-pkt (including ZLP) is
received.

transfer methods are thread-safe.

A transfer can be cancelled at any time.

Signed-off-by: Loic Poulain <loic.poulain@linaro.org>
This commit is contained in:
Loic Poulain 2018-02-05 16:47:12 +01:00 committed by Anas Nashif
commit 16921b0698
2 changed files with 345 additions and 0 deletions

View file

@ -298,4 +298,69 @@ int usb_ep_read_wait(u8_t ep, u8_t *data, u32_t max_data_len,
*/
int usb_ep_read_continue(u8_t ep);
/**
* Callback function signature for transfer completion.
*/
typedef void (*usb_transfer_callback)(u8_t ep, int tsize, void *priv);
/* USB transfer flags */
#define USB_TRANS_READ BIT(0) /** Read transfer flag */
#define USB_TRANS_WRITE BIT(1) /** Write transfer flag */
#define USB_TRANS_NO_ZLP BIT(2) /** No zero-length packet flag */
/**
* @brief Transfer management endpoint callback
*
* If a USB class driver wants to use high-level transfer functions, driver
* needs to register this callback as usb endpoint callback.
*/
void usb_transfer_ep_callback(u8_t ep, enum usb_dc_ep_cb_status_code);
/**
* @brief Start a transfer
*
* Start a usb transfer to/from the data buffer. This function is asynchronous
* and can be executed in IRQ context. The provided callback will be called
* on transfer completion (or error) in thread context.
*
* @param[in] ep Endpoint address corresponding to the one
* listed in the device configuration table
* @param[in] data Pointer to data buffer to write-to/read-from
* @param[in] dlen Size of data buffer
* @param[in] flags Transfer flags (USB_TRANS_READ, USB_TRANS_WRITE...)
* @param[in] cb Function called on transfer completion/failure
* @param[in] priv Data passed back to the transfer completion callback
*
* @return 0 on success, negative errno code on fail.
*/
int usb_transfer(u8_t ep, u8_t *data, size_t dlen, unsigned int flags,
usb_transfer_callback cb, void *priv);
/**
* @brief Start a transfer and block-wait for completion
*
* Synchronous version of usb_transfer, wait for transfer completion before
* returning.
*
* @param[in] ep Endpoint address corresponding to the one
* listed in the device configuration table
* @param[in] data Pointer to data buffer to write-to/read-from
* @param[in] dlen Size of data buffer
* @param[in] flags Transfer flags
*
* @return number of bytes transferred on success, negative errno code on fail.
*/
int usb_transfer_sync(u8_t ep, u8_t *data, size_t dlen, unsigned int flags);
/**
* @brief Cancel any ongoing transfer on the specified endpoint
*
* @param[in] ep Endpoint address corresponding to the one
* listed in the device configuration table
*
* @return 0 on success, negative errno code on fail.
*/
void usb_cancel_transfer(u8_t ep);
#endif /* USB_DEVICE_H_ */

View file

@ -93,10 +93,38 @@
#define MAX_NUM_REQ_HANDLERS (4)
#define MAX_STD_REQ_MSG_SIZE 8
#define MAX_NUM_TRANSFERS 4 /** Max number of parallel transfers */
/* Default USB control EP, always 0 and 0x80 */
#define USB_CONTROL_OUT_EP0 0
#define USB_CONTROL_IN_EP0 0x80
struct usb_transfer_data {
/** endpoint associated to the transfer */
u8_t ep;
/** Transfer status */
int status;
/** Transfer read/write buffer */
u8_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;
};
static void usb_transfer_work(struct k_work *item);
static struct usb_dev_priv {
/** Setup packet */
struct usb_setup_packet setup;
@ -122,6 +150,8 @@ static struct usb_dev_priv {
bool enabled;
/** Currently selected configuration */
u8_t configuration;
/** Transfer list */
struct usb_transfer_data transfer[MAX_NUM_TRANSFERS];
} usb_dev;
/*
@ -937,6 +967,12 @@ int usb_enable(struct usb_cfg_data *config)
return ret;
}
/* init transfer slots */
for (i = 0; i < MAX_NUM_TRANSFERS; i++) {
k_work_init(&usb_dev.transfer[i].work, usb_transfer_work);
k_sem_init(&usb_dev.transfer[i].sem, 1, 1);
}
/* enable control EP */
ret = usb_dc_ep_enable(USB_CONTROL_OUT_EP0);
if (ret < 0)
@ -1004,3 +1040,247 @@ int usb_ep_read_continue(u8_t ep)
{
return usb_dc_ep_read_continue(ep);
}
/* Transfer management */
static struct usb_transfer_data *usb_ep_get_transfer(u8_t ep)
{
int i;
for (i = 0; i < ARRAY_SIZE(usb_dev.transfer); i++) {
if (usb_dev.transfer[i].ep == ep) {
return &usb_dev.transfer[i];
}
}
return NULL;
}
static void usb_transfer_work(struct k_work *item)
{
struct usb_transfer_data *trans;
int ret = 0, bytes;
u8_t ep;
trans = CONTAINER_OF(item, struct usb_transfer_data, work);
ep = trans->ep;
if (trans->status != -EBUSY) {
/* transfer cancelled or already completed */
goto done;
}
if (trans->flags & USB_TRANS_WRITE) {
if (!trans->bsize) {
if (!(trans->flags & USB_TRANS_NO_ZLP)) {
usb_dc_ep_write(ep, NULL, 0, NULL);
}
trans->status = 0;
goto done;
}
ret = usb_dc_ep_write(ep, trans->buffer, trans->bsize, &bytes);
if (ret) {
/* 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) {
/* 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(&trans->work);
return;
}
SYS_LOG_DBG("transfer done, ep=%02x, status=%d, size=%u\n",
trans->ep, trans->status, trans->tsize);
trans->cb = NULL;
k_sem_give(&trans->sem);
/* Transfer completion callback */
cb(ep, tsize, priv);
}
}
void usb_transfer_ep_callback(u8_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_IN) {
u32_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 {
u8_t data;
usb_dc_ep_read_wait(ep, &data, 1, &bytes);
} while (bytes);
SYS_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(&trans->work);
}
}
int usb_transfer(u8_t ep, u8_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;
SYS_LOG_DBG("transfer start, ep=%02x, data=%p, dlen=%d\n",
ep, data, dlen);
key = irq_lock();
for (i = 0; i < MAX_NUM_TRANSFERS; i++) {
if (!k_sem_take(&usb_dev.transfer[i].sem, K_NO_WAIT)) {
trans = &usb_dev.transfer[i];
break;
}
}
if (!trans) {
SYS_LOG_ERR("no transfer slot available\n");
ret = -ENOMEM;
goto done;
}
if (trans->status == -EBUSY) {
/* A transfer is already ongoing and not completed */
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(&trans->work);
} else {
/* ready to read, clear NAK */
usb_dc_ep_read_continue(ep);
}
done:
irq_unlock(key);
return ret;
}
void usb_cancel_transfer(u8_t ep)
{
struct usb_transfer_data *trans;
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(&trans->work);
done:
irq_unlock(key);
}
struct usb_transfer_sync_priv {
int tsize;
struct k_sem sem;
};
static void usb_transfer_sync_cb(u8_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(u8_t ep, u8_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;
}