usb: improve and expose the sync/async write API

* Properly define the callback time of a write/commit as the time when
  the pipe is available again for further writing.
* Refuse commits when writes are pending; instead, enforce a strict
  order of finishing writes before committing, which makes sense since
  consecutive writes are ordered this way already.
* Properly support callbacks for writes and for commits.
* Define the synchronous APIs in terms of waiting until the callbacks
  for equivalent asynchronous functions are invoked (plus initial
  waiting for pipes to be ready).
This commit is contained in:
Lephe 2021-04-29 16:16:26 +02:00
parent 4147236343
commit 392d033e4a
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
3 changed files with 271 additions and 171 deletions

View File

@ -68,6 +68,33 @@ typedef struct usb_interface_endpoint {
} usb_interface_endpoint_t;
//---
// General functions
//---
/* Error codes for USB functions */
enum {
/* There are no interfaces */
USB_OPEN_NO_INTERFACE = 1,
/* There are more interfaces than supported (16) */
USB_OPEN_TOO_MANY_INTERFACES,
/* There are not enough endpoint numbers for every interface, or there
are not enough pipes to set them up */
USB_OPEN_TOO_MANY_ENDPOINTS,
/* There is not enough FIFO memory to use the requested buffer sizes */
USB_OPEN_NOT_ENOUGH_MEMORY,
/* Information is missing, such as buffer size for some endpoints */
USB_OPEN_MISSING_DATA,
/* Invalid parameters: bad endpoint numbers, bad buffer sizes... */
USB_OPEN_INVALID_PARAMS,
/* This pipe is busy (returned by usb_write_async()) */
USB_WRITE_BUSY,
/* This pipe is busy (returned by usb_commit_async()) */
USB_COMMIT_BUSY,
};
/* usb_open(): Open the USB link
This function opens the USB link and notifies the host that the device is
@ -107,6 +134,93 @@ void usb_open_wait(void);
main menu before using it again. */
void usb_close(void);
//---
// Pipe writing API
//---
/* usb_write_sync(): Synchronously write to a USB pipe
This functions writes (size) bytes of (data) into the specified pipe, by
units of (unit_size) bytes. The unit size must be 1, 2 or 4, and both (data)
and (size) must be multiples of the unit size. In general, you should try to
use the largest possible unit size, as it will be much faster. In a sequence
of writes that concludes with a commit, all the writes must use the same
unit size.
If the data fits into the pipe, this function returns right away, and the
data is *not* transmitted. Otherwise, data is written until the pipe is
full, at which point it is automatically transmitted. After the transfer,
this function resumes writing, returning only once everything is written.
Even then the last bytes will still not have been transmitted, to allow for
other writes to follow. After the last write in a sequence, use
usb_commit_sync() or usb_commit_async() to transmit the last bytes.
If (use_dma=true), the write is performed wita the DMA instead of the CPU,
which is generally faster.
*WARNING*: Due to a current limitation in the DMA API, the same DMA channel
is used for all DMA-based writes to USB pipes. Do not write to two USB pipes
with DMA at the same time!
If the pipe is busy due to an ongoing asynchronous write or commit, this
function waits for the operation to complete and proceeds normally.
@pipe Pipe to write into
@data Source data (unit_size-aligned)
@size Size of source (multiple of unit_size)
@unit_size FIFO access size (must be 1, 2, or 4)
@dma Whether to use the DMA to perform the write
-> Returns an error code (0 on success). */
int usb_write_sync(int pipe, void const *data, int size, int unit_size,
bool use_dma);
/* usb_write_async(): Asynchronously write to a USB pipe
This function is similar to usb_write_sync(), but it only starts the writing
and returns immediately without ever waiting. The writing then occurs in the
background of the calling code, and the caller is notified through a
callback when it completes. Use GINT_CALL() to create a callback or pass
GINT_CALL_NULL.
If the pipe is busy due to a previous asynchronous write, this function
returns USB_PIPE_BUSY. When called with (use_dma=true), it returns as soon
as the DMA starts, without even a guarantee that the first few bytes have
been written.
There is no guarantee that the write is complete until the callback is
called, however calling again with data=NULL and size=0 can be used to
determine whether the write has finished, since it will return 0 if the pipe
is idle and USB_PIPE_BUSY otherwise.
@pipe Pipe to write into
@data Source data (unit_size-aligned)
@size Size of source (multiple of unit_size)
@unit_size FIFO access size (must be 1, 2, or 4)
@dma Whether to use the DMA to perform the write
@callback Optional callback to invoke when the write completes
-> Returns an error code (0 on success). */
int usb_write_async(int pipe, void const *data, int size, int unit_size,
bool use_dma, gint_call_t callback);
/* usb_commit_sync(): Synchronously commit a write
This function waits for any pending write on the pipe to finish, then
transfers whatever data is left, and returns when the transfer completes. */
void usb_commit_sync(int pipe);
/* usb_commit_async(): Asynchronously commit a write
This function commits the specified pipe, causing the pipe to transfer
written data in the pipe.
If the pipe is currently busy due to an ongoing write or commit, it returns
USB_COMMIT_BUSY. You should call usb_commit_async() when the pipe is ready,
which is either when the previous synchronous call returns, or when the
callback of the previous asynchronous call is invoked.
This function returns immediately and invokes (callback) when the transfer
of the remaining data completes. */
int usb_commit_async(int pipe, gint_call_t callback);
//---
// USB debugging log
//---

View File

@ -1,6 +1,7 @@
#include <gint/usb.h>
#include <gint/mpu/usb.h>
#include <gint/clock.h>
#include <gint/dma.h>
#include <gint/defs/util.h>
#include <gint/std/string.h>
#include "usb_private.h"
@ -84,9 +85,7 @@ static void pipe_mode(pipect_t ct, int pipe, int mode, int size)
return;
}
/* Set PID to NAK to clear the toggle bit, then BUF */
USB.PIPECTR[pipe-1].PID = 0;
USB.PIPECTR[pipe-1].SQCLR = 1;
/* Set PID to BUF */
USB.PIPECTR[pipe-1].PID = 1;
/* RCNT=0 REW=0 DCLRM=0 DREQE=0 MBW=size BIGEND=1 */
@ -116,17 +115,20 @@ struct transfer {
/* Size of data left to transfer */
int size;
/* Size of data currently in the FIFO (less than the FIFO capacity) */
int used;
uint16_t used;
/* Data sent in the last transfer not yet finished by finish_round() */
uint16_t flying;
/* Write size */
uint8_t unit_size;
/* Whether the data has been committed to a transfer */
bool committed;
/* Whether to use the DMA */
bool dma;
/* Callback at the end of the transfer */
/* Callback to be invoked at the end of the current write or commit
(both cannot exist at the same time) */
gint_call_t callback;
};
/* Operations to be continued whenever buffers get empty */
/* Multi-round operations to be continued whenever buffers are ready */
GBSS static struct transfer volatile pipe_transfers[10];
void usb_pipe_init_transfers(void)
@ -147,31 +149,60 @@ static void write_32(uint32_t const *data, int size, uint32_t volatile *FIFO)
for(int i = 0; i < size; i++) *FIFO = data[i];
}
/* Commit the pipe if there is no data left and the commit flag is set */
static void maybe_commit(struct transfer volatile *t, int pipe)
/* Check whether a pipe is busy with a multi-round write or a transfer */
GINLINE static bool pipe_busy(int pipe)
{
/* The DCP is always committed immediately and with CCPL */
if(pipe == 0) return;
pipect_t ct = pipect(pipe);
/* Multi-round write still not finished */
if(pipe_transfers[pipe].data) return true;
/* Transfer in progress */
if(pipe && !USB.PIPECTR[pipe-1].BSTS) return true;
/* Callback for a just-finished transfer not yet called */
if(pipe_transfers[pipe].flying) return true;
/* All good */
return false;
}
/* The buffer is committed automatically if full because continuous
mode is enabled. Manually commit if no data is left. */
if(t->committed && !t->data)
/* Size of a pipe's buffer area, in bytes */
static int pipe_bufsize(int pipe)
{
if(pipe == 0) return USB.DCPMAXP.MXPS;
USB.PIPESEL.PIPESEL = pipe;
return (USB.PIPEBUF.BUFSIZE + 1) * 64;
}
/* finish_round(): Update transfer logic after a write round completes
This function is called when a write round completes, either by the handler
of the BEMP interrupt if the round filled the FIFO, or by the handler of the
DMA transfer the or write_round() function itself if it didn't.
It the current write operation has finished with this round, this function
invokes the write_async callback. */
static void finish_round(struct transfer volatile *t, int pipe)
{
/* Update the pointer as a result of the newly-finished write */
t->used += t->flying;
t->data += t->flying;
t->size -= t->flying;
t->flying = 0;
/* Account for auto-transfers */
if(t->used == pipe_bufsize(pipe)) t->used = 0;
/* Invoke the callback at the end */
if(t->size == 0)
{
if(ct == D0F) USB.D0FIFOCTR.BVAL = 1;
if(ct == D1F) USB.D1FIFOCTR.BVAL = 1;
t->committed = false;
usb_log("[PIPE%d] Committed transfer\n", pipe);
t->data = NULL;
if(t->callback.function) gint_call(t->callback);
}
}
/* write_round(): Write up to a FIFO's worth of data to a pipe
Returns true if this last write will empty the queue, false if further
writes are required. When writing with the DMA, returning true does not
imply that the pipe can be accessed. */
static bool write_round(struct transfer volatile *t, int pipe)
If this is a partial round (FIFO not going to be full), finish_round() is
invoked after the write. Otherwise the FIFO is transmitted automatically and
the BEMP handler will call finish_round() after the transfer. */
static void write_round(struct transfer volatile *t, int pipe)
{
pipect_t ct = pipect(pipe);
void volatile *FIFO = NULL;
@ -183,52 +214,45 @@ static bool write_round(struct transfer volatile *t, int pipe)
if(pipe) pipe_mode(ct, pipe, 1, t->unit_size);
/* Amount of data that can be transferred in a single run */
int bufsize=64, available=64;
if(pipe != 0)
{
USB.PIPESEL.PIPESEL = pipe;
bufsize = (USB.PIPEBUF.BUFSIZE + 1) * 64;
available = bufsize - t->used;
}
int available = pipe_bufsize(pipe) - (pipe == 0 ? 0 : t->used);
int size = min(t->size, available);
t->flying = size;
/* If this is a partial write (size < available), call finish_round()
after the copy to notify the user that the pipe is ready. Otherwise,
a USB transfer will occur and the BEMP handler will do it. */
bool partial = (size < available);
if(t->dma)
{
/* TODO: DMA support in usb_pipe_write(), write_round() */
/* After the DMA starts the code below will update pointers for
the next iteration */
// dma_start(X, Y, Z,
// GINT_CALL(maybe_commit, (void *)t, pipe));
/* TODO: USB: Can we use 32-byte DMA transfers? */
int block_size = DMA_1B;
if(t->unit_size == 2) block_size = DMA_2B, size >>= 1;
if(t->unit_size == 4) block_size = DMA_4B, size >>= 2;
gint_call_t callback = !partial ? GINT_CALL_NULL :
GINT_CALL(finish_round, (void *)t, pipe);
/* TODO: DMA support in usb_write_async()/write_round() */
/* TODO: USB: Don't use a fixed DMA channel */
dma_transfer_async(3, block_size, size,
t->data, DMA_INC, (void *)FIFO, DMA_FIXED, callback);
}
else
{
if(t->unit_size == 1) write_8(t->data, size, FIFO);
if(t->unit_size == 2) write_16(t->data, size >> 1, FIFO);
if(t->unit_size == 4) write_32(t->data, size >> 2, FIFO);
if(partial) finish_round(t, pipe);
}
t->used += size;
t->data += size;
t->size -= size;
if(t->used == bufsize || t->committed) t->used = 0;
if(t->size == 0) t->data = NULL;
/* After a CPU write, commit if needed */
if(!t->dma) maybe_commit(t, pipe);
return (t->data == NULL);
}
/* usb_write_async(): Asynchronously write to a USB pipe */
int usb_write_async(int pipe, void const *data, int size, int unit_size,
bool use_dma, gint_call_t callback)
{
if(pipe_busy(pipe)) return USB_WRITE_BUSY;
struct transfer volatile *t = &pipe_transfers[pipe];
/* Do not initiate a write if a previous write is unfinished or an
ongoing transfer is awaiting completion */
if(t->data || (pipe && !USB.PIPECTR[pipe-1].BSTS))
return USB_WRITE_BUSY;
if(!data || !size) return 0;
t->data = data;
@ -238,54 +262,89 @@ int usb_write_async(int pipe, void const *data, int size, int unit_size,
t->committed = false;
t->callback = callback;
// TODO: Support callback in usb_write_async()
write_round(t, pipe);
/* Set up the Buffer Empty interrupt to refill the buffer when it gets
empty, and be notified when the transfer completes. */
if(pipe) USB.BEMPENB.word |= (1 << pipe);
write_round(t, pipe);
return 0;
}
/* usb_write_sync(): Synchronously write to a USB pipe */
int usb_write_sync(int pipe, void const *data, int size, int unit_size,
bool use_dma)
bool use_dma)
{
struct transfer volatile *t = &pipe_transfers[pipe];
/* Wait for a previous write and/or transfer to finish */
while(t->data || (pipe && !USB.PIPECTR[pipe-1].BSTS)) sleep();
while(pipe_busy(pipe)) sleep();
usb_write_async(pipe, data, size, unit_size, use_dma, GINT_CALL_NULL);
volatile int flag = 0;
usb_write_async(pipe, data, size, unit_size, use_dma,
GINT_CALL_SET(&flag));
while(!flag) sleep();
/* Wait for the write to finish (but not the transfer) */
while(t->data) sleep();
return 0;
}
void usb_commit_async(int pipe, gint_call_t callback)
int usb_commit_async(int pipe, gint_call_t callback)
{
struct transfer volatile *t = &pipe_transfers[pipe];
if(pipe_busy(pipe)) return USB_COMMIT_BUSY;
/* TODO: USB: Commit on the DCP? */
if(pipe == 0) return 0;
/* Commiting an empty pipe is a no-op */
if(t->used == 0)
{
if(callback.function) gint_call(callback);
return 0;
}
/* Set BVAL=1 and inform the BMEP handler of the commitment with the
committed flag; the handler will invoke the commit callback */
t->committed = true;
t->callback = callback;
/* Commit the pipe if writes have been completed already */
maybe_commit(t, pipe);
pipect_t ct = pipect(pipe);
if(ct == D0F) USB.D0FIFOCTR.BVAL = 1;
if(ct == D1F) USB.D1FIFOCTR.BVAL = 1;
usb_log("[PIPE%d] Committed transfer\n", pipe);
return 0;
}
void usb_commit_sync(int pipe)
{
volatile int flag = 0;
int rc = 0;
/* Wait until the pipe is free, then commit */
do rc = usb_commit_async(pipe, GINT_CALL_SET(&flag));
while(rc == USB_COMMIT_BUSY);
/* Wait until the commit completes */
while(!flag) sleep();
}
/* usb_pipe_write_bemp(): Callback for the BEMP interrupt on a pipe */
void usb_pipe_write_bemp(int pipe)
{
/* Eliminate interrupts that occur when the pipe is set up but no
transfer is occurring */
struct transfer volatile *t = &pipe_transfers[pipe];
if(!t->data) return;
bool complete = write_round(t, pipe);
if(!complete) return;
if(t->committed)
{
/* Finish transfer, disable interrupt, reset logic */
t->committed = false;
t->used = 0;
USB.BEMPENB.word &= ~(1 << pipe);
USB.BEMPENB.word &= ~(1 << pipe);
if(t->callback.function) gint_call(t->callback);
if(t->callback.function) gint_call(t->callback);
}
else
{
/* Finish a round; if there is more data, keep going */
finish_round(t, pipe);
if(t->data) write_round(t, pipe);
}
}

View File

@ -38,26 +38,6 @@ void usb_configure_log(void);
successful usb_open(), or a context restore in the USB driver. */
void usb_configure(void);
/* Error codes for USB functions */
enum {
/* There are no interfaces */
USB_OPEN_NO_INTERFACE = 1,
/* There are more interfaces than supported (16) */
USB_OPEN_TOO_MANY_INTERFACES,
/* There are not enough endpoint numbers for every interface, or there
are not enough pipes to set them up */
USB_OPEN_TOO_MANY_ENDPOINTS,
/* There is not enough FIFO memory to use the requested buffer sizes */
USB_OPEN_NOT_ENOUGH_MEMORY,
/* Information is missing, such as buffer size for some endpoints */
USB_OPEN_MISSING_DATA,
/* Invalid parameters: bad endpoint numbers, bad buffer sizes... */
USB_OPEN_INVALID_PARAMS,
/* A write is already pending on this pipe */
USB_WRITE_BUSY,
};
/* endpoint_t: Driver information for each open endpoint in the device
There is one such structure for all 16 configurable endpoints, for each
@ -103,6 +83,28 @@ endpoint_t *usb_configure_endpoint(int endpoint);
//---
// Pipe operations
//
// When writing to a pipe, the general workflow is as follows:
//
// 1. The user performs a write of a block of memory of any size. Because the
// FIFO for the pipe only has a limited size, the driver splits the write
// into "rounds" of the size of the FIFO.
//
// The rounds are written to the FIFO. If the FIFO is full, the write
// continues until the FIFO can be accessed again (often after the contents
// of the FIFO have been transmitted, except in double-buffer mode).
//
// If the last round is smaller than the size of the FIFO, the data is not
// transmitted; this allows the user to perform another write immediately.
//
// 2. The user performs more writes, each of which are split into rounds, with
// each round possibly triggering a transfer (if the FIFO is full). Each
// write only finishes after all the data is written and the pipe is
// available for more writing.
//
// 3. After the last write, the user *commits* the pipe, causing any data
// remaining in the FIFO to be transferred even if the FIFO is not full. The
// commit operation finishes when the pipe is writable again.
//---
/* usb_pipe_configure(): Configure a pipe when opening the connection */
@ -117,81 +119,6 @@ void usb_pipe_mode_read(int pipe, int read_size);
/* usb_pipe_mode_write(): Set a pipe in write mode */
void usb_pipe_mode_write(int pipe, int write_size);
/* usb_write_sync(): Synchronously write to a USB pipe
This functions writes (size) bytes of (data) into the specified pipe, by
units of (unit_size) bytes. The unit size must be 1, 2 or 4, and both (data)
and (size) must be multiples of the unit size. In general, you should try to
use the largest possible unit size, as it will be much faster. In a sequence
of writes that concludes with a commit, all the writes must use the same
unit size.
If the data fits into the pipe, this function returns right away, and the
data is *not* transmitted. Otherwise, data is written until the pipe is
full, at which point it is automatically transmitted. After the transfer,
this function resumes writing, returning only once everything is written.
Even then the last bytes will still not have been transmitted, to allow for
other writes to follow. After the last write in a sequence, use
usb_commit_sync() or usb_commit_async() to transmit the last bytes.
If (use_dma=true), the write is performed wita the DMA instead of the CPU,
which is generally faster.
If the pipe is busy due to a previous asynchronous write, this function
waits for the previous write to finish before proceeding normally.
@pipe Pipe to write into
@data Source data (unit_size-aligned)
@size Size of source (multiple of unit_size)
@unit_size FIFO access size (must be 1, 2, or 4)
@dma Whether to use the DMA to perform the write
-> Returns an error code (0 on success). */
int usb_write_sync(int pipe, void const *data, int size, int unit_size,
bool use_dma);
/* usb_write_async(): Asynchronously write to a USB pipe
This function is similar to usb_write_sync(), but it only starts the writing
and returns immediately without ever waiting. The writing then occurs in the
background of the calling code, and the caller is notified through a
callback when it completes. Use GINT_CALL() to create a callback or pass
GINT_CALL_NULL.
If the pipe is busy due to a previous asynchronous write, this function
returns USB_PIPE_BUSY. When called with (use_dma=true), it returns as soon
as the DMA starts, without even a guarantee that the first few bytes have
been written.
There is no guarantee that the write is complete until the callback is
called, however calling again with data=NULL and size=0 can be used to
determine whether the write has finished, since it will return 0 if the pipe
is idle and USB_PIPE_BUSY otherwise.
@pipe Pipe to write into
@data Source data (unit_size-aligned)
@size Size of source (multiple of unit_size)
@unit_size FIFO access size (must be 1, 2, or 4)
@dma Whether to use the DMA to perform the write
@callback Optional callback to invoke when the write completes
-> Returns an error code (0 on success). */
int usb_write_async(int pipe, void const *data, int size, int unit_size,
bool use_dma, gint_call_t callback);
/* usb_commit_sync(): Synchronously commit a write
This function waits for any pending write on the pipe to finish, then
transfers whatever data is left, and returns when the transfer completes.
@pipe Pipe that has been used in previous usb_write_*() calls */
void usb_commit_sync(int pipe);
/* usb_commit_async(): Asynchronously commit a write
This function commits the specified pipe, causing the pipe to transfer
written data as soon as all the writes complete. It returns immediately and
instead the specified callback is invoked when the transfer completes. */
void usb_commit_async(int pipe, gint_call_t callback);
/* usb_pipe_write_bemp(): Callback for the BEMP interrupt on a pipe */
void usb_pipe_write_bemp(int pipe);