From 392d033e4ae216811f9679a3322b77f0685251fa Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 29 Apr 2021 16:16:26 +0200 Subject: [PATCH] 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). --- include/gint/usb.h | 114 +++++++++++++++++++++++ src/usb/pipes.c | 211 +++++++++++++++++++++++++++--------------- src/usb/usb_private.h | 117 +++++------------------ 3 files changed, 271 insertions(+), 171 deletions(-) diff --git a/include/gint/usb.h b/include/gint/usb.h index 0a79a96..d96664a 100644 --- a/include/gint/usb.h +++ b/include/gint/usb.h @@ -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 //--- diff --git a/src/usb/pipes.c b/src/usb/pipes.c index 4df0190..743cb0c 100644 --- a/src/usb/pipes.c +++ b/src/usb/pipes.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #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); + } } diff --git a/src/usb/usb_private.h b/src/usb/usb_private.h index 27d6195..af6ff00 100644 --- a/src/usb/usb_private.h +++ b/src/usb/usb_private.h @@ -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);