diff --git a/src/usb/asyncio.h b/src/usb/asyncio.h new file mode 100644 index 0000000..05b4e53 --- /dev/null +++ b/src/usb/asyncio.h @@ -0,0 +1,140 @@ +//--- +// gint:usb:asyncio - Asynchronous I/O common definitions +//--- + +#ifndef GINT_USB_ASYNCIO +#define GINT_USB_ASYNCIO + +#include +#include + +/* Data tracking the progress of a multi-part multi-round async I/O operation. + + * Multi-part refers to writes being constructed over several calls to + write(2) followed by a "commit" with sync(2) (for async file descriptors; + synchronous file descriptors are committed at every write). + * Multi-round refers to the operation interacting multiple times with + hardware in order to communicate the complete data. + + The process of performing such an I/O operation, as tracked by this + structure and use throughout gint, is as follows. For a write: + + WRITING ---------------------. + ^ | | HW buffer + Start writing | | Not full | full: start + | | | transmission + write(2) | v v + --> IDLE ------------------> PENDING <------------- FLYING-WRITE + ^ ^ | DONE interrupt + | DONE write(2) | | + | interrupt | | + | | | Data exhausted + | sync(2): start | v + FLYING-COMMIT <------------ IN-PROGRESS + transmission + + Initially the operation is in the IDLE state. When a write(2) is issued, it + interacts with hardware then transitions to the IN-PROGRESS state, where it + remains for any subsequent write(2). A sync(2) will properly commit data to + the hardware, finish the operation and return to the IDLE state. + + The FLYING-WRITE and FLYING-COMMIT states refer to waiting periods, after + issuing hardware commands, during which hardware communicates. Usually an + interrupt signals when hardware is ready to resume work. + + Note that in a series of write(2), hardware is only instructed to send data + once the hardware buffer is full. Therefore, a write(2) might transition + directly from IDLE or IN-PROGRESS, to PENDING, to IN-PROGRESS, without + actually communicating with the outside world. + + The invariants and meaning for each state are as follow: + + State Characterization Description + ============================================================================ + IDLE type == ASYNCIO_NONE No I/O operation + PENDING data_w && !flying_w \ Ready to write pending data + && round_size == 0 + WRITING round_size > 0 CPU/DMA write to HW in progress + FLYING-WRITE flying_w && !committed_w HW transmission in progress + IN-PROGRESS data_w != NULL && !flying_w Waiting for write(2) or sync(2) + FLYING-COMMIT flying_w && committed_w HW commit in progress + ============================================================================ + + For a read: + IN interrupt + --> IDLE-EMPTY --------------> IDLE-READY + | \ | ^ + read(2) | \ Transaction read(2) | | Buffer full with + | \ exhausted | | transaction not exhausted + | '----<----------. | | + | \ | | + v IN interrupt \ v | .---. Read from + WAITING ------------------> READING v hardware + '---' + + On this diagram, the right side indicates the presence of data to read from + hardware while the bottom side indicates a read(2) request by the user. + Notice the diagonal arrow back to IDLE-EMPTY, which means that read(2) will + always return at the end of a transaction even if the user-provided buffer + is not full (to avoid waiting). + + The invariants and meaning for each state are as follow: + + State Characterization Description + ============================================================================ + IDLE-EMPTY type == ASYNCIO_NONE No I/O operation + IDLE-READY !data_r && buffer_size > 0 Hardware waiting for us to read + WAITING data_r && !buffer_size Waiting for further HW data + READING round_size > 0 DMA/CPU read from HW in progress + ============================================================================ + + States can be checked and transitioned with the API functions below. */ + +enum { ASYNCIO_NONE, ASYNCIO_READ, ASYNCIO_WRITE }; + +typedef struct +{ + /** User-facing information **/ + + /* Direction of I/O operation */ + uint8_t type; + /* Whether the DMA should be used for hardware access */ + bool dma; + /* Whether the data has been committed by sync(2) [write] */ + bool committed_w; + /* Operation's unit size (meaning depends on hardware) */ + uint8_t unit_size; + + union { + /* Address of data to transfer, incremented gradually [write] */ + void const *data_w; + /* Address of buffer to store data to, incremented gradually [read] */ + void *data_r; + }; + /* Size of data left to transfer / buffer space available */ + int size; + /* Callback at the end of the current write, final commit, or read */ + gint_call_t callback; + + /** Hardware state information **/ + + /* Size of data currently in the hardware buffer */ + uint16_t buffer_used; + /* Size of data being read/written in the current round (which may itself + be asynchronous if it's using the DMA) */ + uint16_t round_size; + /* Hardware resource being used for access (meaning depends on hardware). + Usually, this is assigned during hardware transactions, ie.: + - During a write, a controller is assigned when leaving the IDLE state + and returned when re-entering the IDLE state. + - During a read, a controller is assigne when leaving the IDLE-EMPTY + state and returned when re-entering the IDLE-EMPTY state. */ + uint8_t controller; + /* Whether a hardware operation is in progress ("flying" write states) */ + bool flying_w; + +} asyncio_op_t; + +/* */ + +#endif /* GINT_USB_ASYNCIO */ diff --git a/src/usb/pipes.c b/src/usb/pipes.c index 3161e2e..b11973b 100644 --- a/src/usb/pipes.c +++ b/src/usb/pipes.c @@ -6,6 +6,7 @@ #include +#include "asyncio.h" #include "usb_private.h" #define USB SH7305_USB @@ -173,39 +174,19 @@ static void fifo_unbind(fifo_t ct) /* Current operation waiting to be performed on each pipe. There are two possible states for a pipe's transfer data: -> Either there is a transfer going on, in which case (data != NULL), - (size != 0), and (used) has no meaning. + (size != 0), and (buffer_used) has no meaning. -> Either there is no transfer going on, and (data = NULL), (size = 0). - A controller is assigned to t->ct when a write first occurs until the pipe - is fully committed. (ct = NOF) indicates an unused pipe, while (ct != NOF) - indicates that stuff has been written and is waiting a commit. + A controller is assigned to t->controller when a write first occurs until + the pipe is fully committed. (ct = NOF) indicates an unused pipe, while + (ct != NOF) indicates that stuff has been written and is waiting a commit. Additionally, between a call to write_round() and the corresponding - finish_write(), the (flying) attribute is set to a non-zero value indicating - how many bytes are waiting for write completion. */ -struct transfer { - /* Address of data to transfer next */ - void const *data; - /* Size of data left to transfer */ - int size; - /* Size of data currently in the FIFO (less than the FIFO capacity) */ - 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; - /* FIFO controller being used for this transfer */ - fifo_t ct; - /* Callback to be invoked at the end of the current write or commit - (both cannot exist at the same time) */ - gint_call_t callback; -}; + finish_write(), the (round_size) attribute is set to a non-zero value + indicating how many bytes are waiting for write completion. */ + /* Multi-round operations to be continued whenever buffers are ready */ -GBSS static struct transfer volatile pipe_transfers[10]; +GBSS static asyncio_op_t volatile pipe_transfers[10]; void usb_pipe_init_transfers(void) { @@ -229,11 +210,11 @@ static void write_32(uint32_t const *data, int size, uint32_t volatile *FIFO) GINLINE static bool pipe_busy(int pipe) { /* Multi-round write still not finished */ - if(pipe_transfers[pipe].data) return true; + if(pipe_transfers[pipe].data_w) 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; + if(pipe_transfers[pipe].round_size) return true; /* All good */ return false; } @@ -241,7 +222,8 @@ GINLINE static bool pipe_busy(int pipe) /* Size of a pipe's buffer area, in bytes */ static int pipe_bufsize(int pipe) { - if(pipe == 0) return USB.DCPMAXP.MXPS; + if(pipe == 0) + return USB.DCPMAXP.MXPS; USB.PIPESEL.PIPESEL = pipe; return (USB.PIPEBUF.BUFSIZE + 1) * 64; @@ -252,15 +234,15 @@ static int pipe_bufsize(int pipe) This function is called when the final round of a transfer has completed, either by the handler of the BEMP interrupt or by the usb_commit_async() function if the pipe is being committed when empty. */ -static void finish_transfer(struct transfer volatile *t, int pipe) +static void finish_transfer(asyncio_op_t volatile *t, int pipe) { /* Free the FIFO controller */ - fifo_unbind(t->ct); - t->ct = NOF; + fifo_unbind(t->controller); + t->controller = NOF; /* Mark the transfer as unused */ - t->committed = false; - t->used = 0; + t->committed_w = false; + t->buffer_used = 0; /* Disable the interrupt */ if(pipe != 0) @@ -278,22 +260,22 @@ static void finish_transfer(struct transfer volatile *t, int pipe) 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) +static void finish_round(asyncio_op_t 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; + t->buffer_used += t->round_size; + t->data_w += t->round_size; + t->size -= t->round_size; + t->round_size = 0; /* Account for auto-transfers */ - if(t->used == pipe_bufsize(pipe)) - t->used = 0; + if(t->buffer_used == pipe_bufsize(pipe)) + t->buffer_used = 0; /* At the end, free the FIFO and invoke the callback. Hold the controller until the pipe is committed */ if(t->size == 0) { - t->data = NULL; + t->data_w = NULL; gint_call(t->callback); } @@ -305,9 +287,9 @@ static void finish_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) +static void write_round(asyncio_op_t volatile *t, int pipe) { - fifo_t ct = t->ct; + fifo_t ct = t->controller; void volatile *FIFO = NULL; if(ct == CF) FIFO = &USB.CFIFO; @@ -316,9 +298,9 @@ static void write_round(struct transfer volatile *t, int pipe) fifo_bind(ct, pipe, FIFO_WRITE, t->unit_size); /* Amount of data that can be transferred in a single run */ - int available = pipe_bufsize(pipe) - (pipe == 0 ? 0 : t->used); + int available = pipe_bufsize(pipe) - (pipe == 0 ? 0 : t->buffer_used); int size = min(t->size, available); - t->flying = size; + t->round_size = 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, @@ -340,14 +322,14 @@ static void write_round(struct transfer volatile *t, int pipe) int channel = (ct == D0F) ? 3 : 4; bool ok = dma_transfer_async(channel, block_size, size, - t->data, DMA_INC, (void *)FIFO, DMA_FIXED, callback); + t->data_w, DMA_INC, (void *)FIFO, DMA_FIXED, callback); if(!ok) USB_LOG("DMA async failed on channel %d!\n", channel); } 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(t->unit_size == 1) write_8(t->data_w, size, FIFO); + if(t->unit_size == 2) write_16(t->data_w, size >> 1, FIFO); + if(t->unit_size == 4) write_32(t->data_w, size >> 2, FIFO); if(partial) finish_round(t, pipe); } @@ -359,22 +341,22 @@ int usb_write_async(int pipe, void const *data, int size, int unit_size, { if(pipe_busy(pipe)) return USB_WRITE_BUSY; - struct transfer volatile *t = &pipe_transfers[pipe]; + asyncio_op_t volatile *t = &pipe_transfers[pipe]; if(!data || !size) return 0; /* Re-use the controller from a previous write if there is one, otherwise try to get a new free one */ /* TODO: usb_write_async(): TOC/TOU race on controller being free */ - fifo_t ct = t->ct; + fifo_t ct = t->controller; if(ct == NOF) ct = fifo_find_available_controller(pipe); if(ct == NOF) return USB_WRITE_NOFIFO; - t->data = data; + t->data_w = data; t->size = size; t->unit_size = (pipe == 0) ? 1 : unit_size; t->dma = use_dma; - t->committed = false; - t->ct = ct; + t->committed_w = false; + t->controller = ct; t->callback = callback; /* Set up the Buffer Empty interrupt to refill the buffer when it gets @@ -421,12 +403,12 @@ int usb_write_sync(int pipe, void const *data, int size, int unit, bool dma) int usb_commit_async(int pipe, gint_call_t callback) { - struct transfer volatile *t = &pipe_transfers[pipe]; + asyncio_op_t volatile *t = &pipe_transfers[pipe]; if(pipe_busy(pipe)) return USB_COMMIT_BUSY; - if(t->ct == NOF) return USB_COMMIT_INACTIVE; + if(t->controller == NOF) return USB_COMMIT_INACTIVE; - t->committed = true; + t->committed_w = true; t->callback = callback; /* TODO: Handle complex commits on the DCP */ @@ -438,17 +420,17 @@ int usb_commit_async(int pipe, gint_call_t callback) } /* Committing an empty pipe ends the transfer on the spot */ - if(t->used == 0) + if(t->buffer_used == 0) { finish_transfer(t, pipe); return 0; } /* Set BVAL=1 and inform the BEMP handler of the commitment with the - committed flag; the handler will invoke finish_transfer() */ - fifo_bind(t->ct, pipe, FIFO_WRITE, t->unit_size); - if(t->ct == D0F) USB.D0FIFOCTR.BVAL = 1; - if(t->ct == D1F) USB.D1FIFOCTR.BVAL = 1; + committed_w flag; the handler will invoke finish_transfer() */ + fifo_bind(t->controller, pipe, FIFO_WRITE, t->unit_size); + if(t->controller == D0F) USB.D0FIFOCTR.BVAL = 1; + if(t->controller == D1F) USB.D1FIFOCTR.BVAL = 1; USB_LOG("[PIPE%d] Committed transfer\n", pipe); return 0; @@ -489,9 +471,9 @@ void usb_commit_sync(int pipe) /* usb_pipe_write_bemp(): Callback for the BEMP interrupt on a pipe */ void usb_pipe_write_bemp(int pipe) { - struct transfer volatile *t = &pipe_transfers[pipe]; + asyncio_op_t volatile *t = &pipe_transfers[pipe]; - if(t->committed) + if(t->committed_w) { finish_transfer(t, pipe); } @@ -499,6 +481,6 @@ void usb_pipe_write_bemp(int pipe) { /* Finish a round; if there is more data, keep going */ finish_round(t, pipe); - if(t->data) write_round(t, pipe); + if(t->data_w) write_round(t, pipe); } }