From 4983849510404976ad68a4d6b9df853dd1b03a65 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sat, 18 Mar 2023 19:09:35 +0100 Subject: [PATCH] usb: consolidate reading logic again, now beta-worthy This looks like it could work in the long term. The only issue that really hasn't been addressed is how to use packet counters to cut transactions when there's no ZLP, but we can leave that for later. --- include/gint/drivers/asyncio.h | 138 ++++++++----- include/gint/usb-ff-bulk.h | 4 +- include/gint/usb.h | 145 ++++++++------ src/usb/asyncio.c | 95 ++++++--- src/usb/classes/ff-bulk.c | 26 ++- src/usb/pipes.c | 342 ++++++++++++++++++++------------- 6 files changed, 468 insertions(+), 282 deletions(-) diff --git a/include/gint/drivers/asyncio.h b/include/gint/drivers/asyncio.h index d52aa7e..d2bfa50 100644 --- a/include/gint/drivers/asyncio.h +++ b/include/gint/drivers/asyncio.h @@ -120,17 +120,33 @@ entire transaction by reading data until they get fewer bytes than requested without running the risk of blocking on the next transaction. - The invariants and meaning for each state are as follow: + The invariants and meaning for each state are as follow. The right side + (presence of a hardware segment) is indicated by `buffer_used >= 0`, while + the bottom side (presence of a read(2) request) is indicated by `size > 0`. + As an exception, IDLE-EMPTY uses `buffer_used == 0` so that the default + zero-initialization of transfer data is sufficient. - State Characterization Description + State Invariant characterization Description ============================================================================ - IDLE-EMPTY type == ASYNCIO_NONE No I/O operation + IDLE-EMPTY type == ASYNCIO_NONE No I/O operation, the pipe is + && buffer_used == 0 idle with no request and no + && size == 0 hardware segment (but we might + && round_size == 0 still be mid-transaction) ---------------------------------------------------------------------------- - IDLE-READY !data_r Hardware segment pending + IDLE-READY type == ASYNCIO_READ There is a hardware segment not + && buffer_used >= 0 marked as complete, but no + && size == 0 read(2) request to consume it + && round_size == 0 ---------------------------------------------------------------------------- - WAITING data_r && !round_size User segment pending + WAITING type == ASYNCIO_READ There is a read(2) request but + && buffer_used < 0 no hardware segment, and either + && size > 0 the request is new or the + && round_size == 0 transaction isn't exhausted ---------------------------------------------------------------------------- - READING round_size > 0 DMA/CPU read round in progress + READING type == ASYNCIO_READ A read round in progress and + && buffer_used >= 0 either the read(2) request or + && size > 0 hardware segment will be + && round_size > 0 exhausted when it ends ============================================================================ The series of asyncio_op function calls for a read is a bit more complicated @@ -147,7 +163,7 @@ | R1 | R2 | R3 | R4 | R5 | R6 | Read rounds +-----------+------+----+-----------+-----------+---------+ | User buffer #1 | User buffer #2 | (short) | User segments - +-----------+------+----+-----------+-----------+---------+ + +------------------+----------------------------+---------+ ^ read(2) ^ read(2) ^ read(2) Reads rounds are exactly the intersections between hardware segments and @@ -159,7 +175,35 @@ enum { ASYNCIO_NONE, ASYNCIO_READ, ASYNCIO_WRITE, ASYNCIO_SYNC }; typedef volatile struct { - /** User-facing information **/ + /* Type of I/O operation (NONE/WRITE/SYNC/READ) */ + uint8_t type; + /* Whether the DMA should be used for hardware access */ + bool dma :1; + /* For reading pipes, whether the transaction is expected to continue with + another hardware segment after the current one */ + bool cont_r :1; + /* For reading pipes, interrupt flag signaling an incoming hardware segment + not yet added to the operation */ + bool interrupt_r :1; + /* For reading pipes, whether the current read call should close the + current hardware segment if all the data is read even if the read call + is not partial */ + bool autoclose_r :1; + /* Hardware resource being used for access (meaning depends on hardware). + Usually, this is assigned for the duration of hardware transaction. + This value is user-managed and not modified by asyncio_op functions. */ + uint8_t controller; + + /* Number of bytes in short buffer (0..3) */ + uint8_t shbuf_size; + /* Short buffer */ + uint32_t shbuf; + + /* Size of data currently in the hardware buffer */ + int16_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; union { /* Address of data to transfer, incremented gradually [write] */ @@ -169,40 +213,10 @@ typedef volatile struct }; /* Size of data left to transfer to satisfy the complete request */ int size; - /* Callback at the end of the current write, final commit, or read */ - gint_call_t callback; /* For reading operations, pointer to total amount of transferred data */ int *realized_size_r; - - /* Type of I/O operation (read/write/fsync) */ - uint8_t type; - /* Whether the DMA should be used for hardware access */ - bool dma; - - /** 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; - /* For reading operations, whether the transaction is expected to continue - with another hardware segment after the current one */ - bool cont_r; - /* For reading operations, whether there is a new hardware segment just - signaled by an interrupt not yet added to the operation */ - bool interrupt_r; - /* Hardware resource being used for access (meaning depends on hardware). - Usually, this is assigned for the duration of hardware transaction. - This value is user-managed and not modified by asyncio_op functions. */ - uint8_t controller; - - /** Internal information **/ - - /* Number of bytes in short buffer (0..3) */ - uint8_t shbuf_size; - /* Short buffer */ - uint32_t shbuf; + /* Callback at the end of the current write, final commit, or read */ + gint_call_t callback; } asyncio_op_t; @@ -238,20 +252,42 @@ void asyncio_op_finish_write_round(asyncio_op_t *op); void asyncio_op_start_sync(asyncio_op_t *op, gint_call_t const *callback); void asyncio_op_finish_sync(asyncio_op_t *op); -/* Start/finish a read(2) call. */ +/* Start a read(2) call. The call will finish automatically when the final + round finishes. If `autoclose` is set, the current hardware segment will + be marked as completed if the round reads it entirely, even if the request + is fulfilled. */ void asyncio_op_start_read(asyncio_op_t *op, void *data, size_t size, - bool use_dma, int *realized_size, gint_call_t const *callback); -void asyncio_op_finish_read(asyncio_op_t *op); + bool use_dma, int *realized_size, bool autoclose, + gint_call_t const *callback); -/* Start/finish a hardware segment `cont` should be true if there will be - another segment in the same transaction. */ +/* Start a hardware segment. `cont` should be true if there will be another + segment in the same transaction. The segment will finish automatically when + it is completely consumed by a read round. */ void asyncio_op_start_read_hwseg(asyncio_op_t *op, size_t size, bool cont); -void asyncio_op_finish_read_hwseg(asyncio_op_t *op); -/* Start/finish a single-block read from hardware. The finish function returns - true if the current hardware segment finished with the round (op->cont_r - indicates whether more segments will follow). */ -void asyncio_op_start_read_round(asyncio_op_t *op, size_t size); -bool asyncio_op_finish_read_round(asyncio_op_t *op); +bool asyncio_op_has_read_call(asyncio_op_t const *op); + +bool asyncio_op_has_read_hwseg(asyncio_op_t const *op); + +/* Start a single-block read from hardware. The requested size is automatically + t->size, however the round may of course be smaller depending on how much + data is available. Returns the round size. */ +int asyncio_op_start_read_round(asyncio_op_t *op); + +enum { + ASYNCIO_HWSEG_EXHAUSTED = 0x01, + ASYNCIO_REQUEST_FINISHED = 0x02, + ASYNCIO_TRANSACTION_EXHAUSTED = 0x04, +}; + +/* Finish a single-block read from hardware. This function also finishes the + current hardware segment and read call if appropriate, *except* that it + doesn't invoke the read(2) callback. You should make a copy of it before + calling and invoke it manually after. Returns a combination of the above + flags indicating what finished along with the round. */ +int asyncio_op_finish_read_round(asyncio_op_t *op); + +/* Cancel a read call. This keeps the hardware segment part intact. */ +void asyncio_op_cancel_read(asyncio_op_t *op); #endif /* GINT_USB_ASYNCIO */ diff --git a/include/gint/usb-ff-bulk.h b/include/gint/usb-ff-bulk.h index deb72d8..2d962b5 100644 --- a/include/gint/usb-ff-bulk.h +++ b/include/gint/usb-ff-bulk.h @@ -130,8 +130,8 @@ void usb_fxlink_set_notifier(void (*notifier_function)(void)); /* usb_ff_bulk_output(): Pipe number for calculator -> host communication */ int usb_ff_bulk_output(void); -/* usb_ff_bulk_in(): Pipe number for host -> calculator communication */ -int usb_ff_bulk_in(void); +/* usb_ff_bulk_input(): Pipe number for host -> calculator communication */ +int usb_ff_bulk_input(void); //--- // Construction and analysis of fxlink protocol messages diff --git a/include/gint/usb.h b/include/gint/usb.h index 89e68c7..bb879fb 100644 --- a/include/gint/usb.h +++ b/include/gint/usb.h @@ -55,6 +55,9 @@ enum { USB_READ_IDLE = -12, /* No FIFO controller is available */ USB_READ_NOFIFO = -13, + + /* (Internal codes) */ + USB_ERROR_ZERO_LENGTH = -100, }; /* usb_open(): Open the USB link @@ -273,47 +276,32 @@ int usb_commit_async(int pipe, gint_call_t callback); /* usb_read_sync(): Synchronously read from a USB pipe This function waits for data to become available on the specified `pipe`, - and then reads up to `size` bytes into `data`. It synchronizes with USB + and then reads up to `size` bytes into `data`. It is "synchronized" with USB transactions on the pipe in the following ways: - 1. If there is no active transaction when it is first called, it blocks - until one starts (which can freeze). - 2. If there is an active transaction but all of its contents were consumed - by a previous read, it is marked as complete before usb_read_sync() - starts waiting. + 1. If there is no active transaction, it waits for one to arrive. + 2. If the active transaction has no data left to read, it is skipped. + 3. If the transaction's data is completely read, the transaction is closed + even if `data` is full. (See usb_read_async() for the alternative.) - The second point has a subtle implication. In an application that always - reads synchronously from a pipe, the "resting" state between transactions - can show the last processed transaction as active with 0 bytes remaining to - read, as processed transactions are not marked as completed until a read - request is fulfilled *partially*. This is for three reasons: + Basically usb_read_sync() returns the next `size` bytes of data that the + calculator receives on the pipe, with a single exception: it doesn't read + across transactions, so if the active transaction has 100 bytes left and you + request 200, you will only get 100. usb_read_sync() only ever returns 0 + bytes if there is an empty transaction, which is rare. - * Only marking transactions as complete upon partial reads makes it possible - to properly detect the end of transactions in asynchronous mode. See - usb_read_async() for more details. - - * It further allows detecting the end of transactions in synchronous mode by - checking for partial reads *or* usb_poll() returning false. - - * Marking transactions with 0 bytes left to read as complete *before* a - synchronous read (instead of after) avoids complications for sync reads - when switching between sync and async reads. It makes async reads handle - the switch with a 0-byte read, instead of the other way round. Thus, sync - reads don't need to worry about switches and can never return 0 bytes. - - It is not an error for usb_read_sync() to return fewer bytes than requested. - If the active USB transaction has fewer bytes left than specified in `size`, - the remaining data will be returned without waiting for another transaction. - (Reads never carry across transactions.) Because usb_read_sync() will ignore - transactions with 0 bytes left, it can never return 0 bytes. + Because this is a blocking function, a call to usb_read_sync() will *FREEZE* + if there is no data to read on the pipe and the host doesn't send any. In + addition, it's not possible to detect how much data is left to read with + usb_read_sync()'s interface. If you want to avoid or debug a freeze, use + use usb_read_async() or set a timeout with usb_read_sync_timeout(). If you + want to read a transaction until the end without knowing its size in + advance, use usb_read_async(). If `use_dma=true`, uses the DMA for transfers. This requires 4-byte alignment on the buffer, size, and number of bytes previously read in the current transaction. - This function will use a FIFO to access the pipe. The FIFO will only be - released once the transaction is completely consumed. - Returns the number of bytes read or a negative error code. */ int usb_read_sync(int pipe, void *data, int size, bool use_dma); @@ -321,46 +309,77 @@ int usb_read_sync(int pipe, void *data, int size, bool use_dma); int usb_read_sync_timeout(int pipe, void *data, int size, bool use_dma, timeout_t const *timeout); +/* Flags for usb_read_async(), see below. */ +enum { + /* Use the DMA for data transfer (requires full 4-byte alignment). */ + USB_READ_USE_DMA = 0x01, + /* Ignore reads of 0 bytes from exhausted transactions. */ + USB_READ_IGNORE_ZEROS = 0x02, + /* Close transactions when exhausted even by non-partial reads. */ + USB_READ_AUTOCLOSE = 0x04, + /* Wait for the read to finish before returning. */ + USB_READ_WAIT = 0x08, + /* Block for a new transaction if none is currenty active. */ + USB_READ_BLOCK = 0x10, +}; + /* usb_read_async(): Asynchronously read from a USB pipe - This function is similar to usb_read_sync() except that it doesn't - synchronize with transactions, resulting in two differences: + This function is similar to usb_read_sync() but it is asynchronous, ie. it + returns after starting the read and then runs in the background. Without + options, usb_read_async() is guaranteed to return quickly without waiting + for communications. The read itself also never blocks unless there is a + disagreement with the host on the size of transactions. - 1. It returns USB_READ_INACTIVE if there is no active transaction. - 2. If there is an active transaction with 0 bytes left to read, it returns 0 - bytes and marks the transaction as complete (unless size == 0). + usb_read_async() is also slightly lower-level than usb_read_sync(). In + particular, it only closes transactions when the data is fully read *and* + the user buffer is not full. If the user requests exactly the number of + bytes left in the transaction, the entire contents will be read but the + transaction will be left in an "active with 0 bytes left" state. Only on the + next read will the transaction be closed. - Like usb_read_sync(), if the exact amount of data left in the transaction is - requested, the transaction will remain active with 0 bytes left to read and - the *next* read will return 0 bytes and mark it as complete. This way, the - end of the transaction can be detected consistently: it is sufficient to - loop until a read call returns fewer bytes than requested. + This behavior is designed so that a read closes a transaction if and only if + it returns fewer bytes than requested, which is useful for detecting the end + of transfers when their size isn't known in advance. - Being asynchronous, this function starts the read process and returns - instantly; 0 on success, an error code otherwise. When the read finishes, - the provided callback is called with `*read_size` set to the number of bytes - read. */ -int usb_read_async(int pipe, void *data, int size, bool use_dma, - int *read_size, gint_call_t callback); + This function returns a preliminary error code. If it is zero, then the + callback will be invoked later with *rc set to the final return value of the + read, which is itself either an error (< 0) or the number of bytes read. -/* usb_poll(): Check whether there is data to read in a pipe + Due to its low-level nature, raw usb_read_async() is a bit impractical to + use and almost always requires repeated calls and callback waits. The + following flags are provided to make it more convenient by incorporating + features of usb_read_sync(): - This function checks whether a transaction is active on the pipe with more - than 0 bytes left to read. Its main purpose is to simplify reading patterns - with usb_read_sync(), in the following ways: + * USB_READ_IGNORE_ZEROS causes usb_read_async() to ignore reads of 0 bytes + from transactions that were exhausted but not closed. + * USB_READ_AUTOCLOSE causes usb_read_async() to always close transactions + when exhausted even if the read is not partial. This is useful when the + size of the transaction is in fact known in advance. + * USB_READ_WAIT causes usb_read_async() to wait for the end of the transfer + before returning. Unless there is a driver bug or USB_READ_BLOCK is also + set, this cannot cause the function to freeze since the read will always + complete in finite time (returning USB_READ_IDLE if no transaction is + active). When USB_READ_WAIT it set, `callback` and `rc` are ignored; the + callback is not invoked, and the status of the entire read is returned. + * USB_READ_BLOCK causes usb_read_async() to wait for a transaction if none + is active. This can stall the transfer indefinitely if the host doesn't + transmit any data, and freeze the call entirely if USB_READ_WAIT is set. + This option really makes usb_read_async() a synchronous function. + * A timeout is supported (pass NULL to ignore). - 1. When usb_poll() returns true, usb_read_sync() is guaranteed to not block - (unless the host fails to complete a transaction). - 2. After a synchronous read which was completely fulfilled (ie. returned as - many bytes as were requested), usb_poll() will tell whether there is any - more data left. This is useful because if there is no data left, the next - call to usb_read_sync() would block. Thus, it is possible to detect the - end of the current transaction during synchronous reads by checking for - (1) partial reads, and (2) usb_poll() returning false after a full read. + With all options enabled, usb_read_async() becomes functionally identical to + usb_read_sync_timeout(). */ +int usb_read_async(int pipe, void *data, int size, int flags, int *rc, + timeout_t const *timeout, gint_call_t callback); - usb_poll() is of little interest to asynchronous applications since it is - easier to check the return status of usb_read_async(). */ -bool usb_poll(int pipe); +/* usb_read_cancel(): Cancel an asynchronous read + + Once started, an async read will run in the background and keep writing to + the provided buffer. This function cancels the operation so that no further + writes to the buffer are made and the associated memory can be safely + deallocated. */ +void usb_read_cancel(int pipe); //--- // USB debugging functions diff --git a/src/usb/asyncio.c b/src/usb/asyncio.c index 0dee29b..0a0e34d 100644 --- a/src/usb/asyncio.c +++ b/src/usb/asyncio.c @@ -1,5 +1,6 @@ #include #include +#include void asyncio_op_clear(asyncio_op_t *op) { @@ -8,9 +9,9 @@ void asyncio_op_clear(asyncio_op_t *op) bool asyncio_op_busy(asyncio_op_t const *op) { - /* WAITING and READING states are busy */ + /* WAITING and READING states are busy (ie. read(2) call in progress) */ if(op->type == ASYNCIO_READ) - return op->round_size || op->data_r != NULL; + return op->size > 0; /* WRITING, FLYING-WRITE and PENDING states are busy */ if(op->type == ASYNCIO_WRITE) return op->data_w != NULL; @@ -72,28 +73,33 @@ void asyncio_op_finish_sync(asyncio_op_t *op) } void asyncio_op_start_read(asyncio_op_t *op, void *data, size_t size, - bool use_dma, int *realized_size, gint_call_t const *callback) + bool use_dma, int *realized_size, bool autoclose, + gint_call_t const *callback) { + assert(!asyncio_op_has_read_call(op) && size > 0 && !op->round_size); + + op->type = ASYNCIO_READ; op->dma = use_dma; + op->autoclose_r = autoclose; op->data_r = data; - op->callback = *callback; - op->realized_size_r = realized_size; op->size = size; + op->realized_size_r = realized_size; + op->callback = *callback; if(realized_size) *realized_size = 0; } -void asyncio_op_finish_read(asyncio_op_t *op) +bool asyncio_op_has_read_call(asyncio_op_t const *op) { - /* Note: In this function the type may be either READ or NONE depending of - whether there is a hardware segment being processed. */ - gint_call(op->callback); - op->dma = false; - op->data_r = NULL; - op->callback = GINT_CALL_NULL; - op->realized_size_r = NULL; - op->size = 0; + /* WAITING and READING states */ + return (op->type == ASYNCIO_READ) && (op->size > 0); +} + +bool asyncio_op_has_read_hwseg(asyncio_op_t const *op) +{ + /* IDLE-READY and READING states */ + return (op->type == ASYNCIO_READ) && (op->buffer_used >= 0); } void asyncio_op_start_read_hwseg(asyncio_op_t *op, size_t size, bool cont) @@ -103,26 +109,65 @@ void asyncio_op_start_read_hwseg(asyncio_op_t *op, size_t size, bool cont) op->cont_r = cont; } -void asyncio_op_finish_read_hwseg(asyncio_op_t *op) +int asyncio_op_start_read_round(asyncio_op_t *op) { - if(!op->cont_r) - op->type = ASYNCIO_NONE; - op->buffer_used = 0; + op->round_size = (op->size < op->buffer_used ? op->size : op->buffer_used); + return op->round_size; } -void asyncio_op_start_read_round(asyncio_op_t *op, size_t size) +int asyncio_op_finish_read_round(asyncio_op_t *op) { - op->round_size = size; -} + int status = 0; -bool asyncio_op_finish_read_round(asyncio_op_t *op) -{ if(op->realized_size_r) *op->realized_size_r += op->round_size; op->buffer_used -= op->round_size; - op->data_r += op->round_size; + if(op->data_r) + op->data_r += op->round_size; op->size -= op->round_size; op->round_size = 0; - return (op->buffer_used == 0); + bool read_fulfilled = (op->size == 0); + bool hwseg_exhausted = + (op->buffer_used == 0 && (op->autoclose_r || op->size > 0)); + bool transaction_exhausted = hwseg_exhausted && !op->cont_r; + + if(read_fulfilled || transaction_exhausted) { + op->dma = false; + op->autoclose_r = false; + op->data_r = NULL; + op->size = 0; + op->callback = GINT_CALL_NULL; + op->realized_size_r = NULL; + status |= ASYNCIO_REQUEST_FINISHED; + } + if(hwseg_exhausted) { + op->buffer_used = -1; + status |= ASYNCIO_HWSEG_EXHAUSTED; + + if(status & ASYNCIO_REQUEST_FINISHED) { + op->type = ASYNCIO_NONE; + op->buffer_used = 0; + } + } + if(transaction_exhausted) { + status |= ASYNCIO_TRANSACTION_EXHAUSTED; + } + + return status; +} + +void asyncio_op_cancel_read(asyncio_op_t *op) +{ + op->dma = false; + op->data_r = NULL; + op->autoclose_r = false; + op->size = 0; + op->callback = GINT_CALL_NULL; + op->realized_size_r = NULL; + + if(!asyncio_op_has_read_hwseg(op)) { + op->type = ASYNCIO_NONE; + op->buffer_used = 0; + } } diff --git a/src/usb/classes/ff-bulk.c b/src/usb/classes/ff-bulk.c index 39a0555..745ce5f 100644 --- a/src/usb/classes/ff-bulk.c +++ b/src/usb/classes/ff-bulk.c @@ -216,12 +216,18 @@ static void (*recv_handler)(void) = NULL; bool usb_fxlink_handle_messages(usb_fxlink_header_t *header_ptr) { - if(!usb_poll(usb_ff_bulk_input())) + if(!usb_is_open()) return false; /* Read a message header first */ usb_fxlink_header_t header; - int rc = usb_read_sync(usb_ff_bulk_input(), &header, sizeof header, false); + timeout_t tm = timeout_make_ms(1000); + + int rc = usb_read_async(usb_ff_bulk_input(), &header, sizeof header, + USB_READ_IGNORE_ZEROS | USB_READ_AUTOCLOSE | USB_READ_WAIT, NULL, + &tm, GINT_CALL_NULL); + if(rc == USB_READ_IDLE) + return false; if(rc < 0) { USB_LOG("[ff-bulk] Header read failed: %d\n", rc); @@ -233,7 +239,6 @@ bool usb_fxlink_handle_messages(usb_fxlink_header_t *header_ptr) return false; } - header.version = le32toh(header.version); header.size = le32toh(header.size); header.transfer_size = le32toh(header.transfer_size); @@ -243,6 +248,11 @@ bool usb_fxlink_handle_messages(usb_fxlink_header_t *header_ptr) (void)minor; (void)major; + if(major != 1 || minor != 0) { + USB_LOG("[ff-bulk] Invalid message header!\n"); + return usb_fxlink_handle_messages(header_ptr); + } + USB_LOG("[ff-bulk %d.%d] New %.16s.%.16s (%d bytes)\n", major, minor, header.application, header.type, header.size); @@ -272,11 +282,17 @@ void usb_fxlink_set_notifier(void (*notifier_function)(void)) void usb_fxlink_drop_transaction(void) { + int block = USB_READ_BLOCK; + while(1) { - int rc = usb_read_sync(usb_ff_bulk_input(), NULL, 512, false); - /* Break on error or short read */ + timeout_t tm = timeout_make_ms(1000); + int rc = usb_read_async(usb_ff_bulk_input(), NULL, 512, + USB_READ_WAIT | block, NULL, &tm, GINT_CALL_NULL); + + /* Break on error or short read (end of transaction) */ if(rc != 512) break; + block = 0; } } diff --git a/src/usb/pipes.c b/src/usb/pipes.c index 22e5b17..e988f44 100644 --- a/src/usb/pipes.c +++ b/src/usb/pipes.c @@ -5,6 +5,7 @@ #include #include +#include #include #include "usb_private.h" @@ -129,9 +130,10 @@ static fifo_t fifo_find_available_controller(int pipe) if(pipe == 0) return CF; if(USB.D0FIFOSEL.CURPIPE == 0) return D0F; - USB_LOG("D0 is unavailable!\n"); + // USB_LOG("D0 is unavailable!\n"); if(USB.D1FIFOSEL.CURPIPE == 0) return D1F; - USB_LOG("D1 is unavailable!\n"); + // USB_LOG("D1 is unavailable!\n"); + USB_LOG("No controller is unavailable!\n"); return NOF; } @@ -467,7 +469,7 @@ int usb_commit_async(int pipe, gint_call_t callback) USB.BEMPENB |= (1 << pipe); if(t->controller == D0F) USB.D0FIFOCTR.BVAL = 1; if(t->controller == D1F) USB.D1FIFOCTR.BVAL = 1; - USB_LOG("[PIPE%d] Committed transfer\n", pipe); + // USB_LOG("[PIPE%d] Committed transfer\n", pipe); return 0; } @@ -524,93 +526,95 @@ void usb_pipe_write_bemp(int pipe) // Reading operations //--- -static int handle_incoming_hwseg(asyncio_op_t *t, int pipe); - #ifdef GINT_USB_DEBUG -static void USB_LOG_TR(char const *prefix, asyncio_op_t *t) +static void USB_LOG_TR(char const *p, asyncio_op_t *t, char const *fmt, ...) { - USB_LOG("%s: %s buf=%d%s%s req=%d/%d", - prefix, - t->type == ASYNCIO_READ ? "READ" : "NONE", + int E = USB.INTENB0.BRDYE; + USB.INTENB0.BRDYE = 0; + + char str[128]; + snprintf(str, sizeof str - 1, "%s: %s buf=%d%s%s req=%d/%d%s ", + p, t->type == ASYNCIO_READ ? "READ" : "NONE", t->buffer_used, t->cont_r ? "+":"", t->interrupt_r ? "!":"", - t->round_size, t->size); + t->round_size, t->size, t->autoclose_r ? "#" : ""); + + va_list args; + va_start(args, fmt); + int l = strlen(str); + vsnprintf(str + l, sizeof str - 1 - l, fmt, args); + va_end(args); + + strcat(str, "\n"); + USB_LOG("%s", str); + + /* if(cpu_getSR().IMASK == 0) { + extern void usb_fxlink_text(char const *str, int size); + usb_fxlink_text(str, 0); + } */ + + USB.INTENB0.BRDYE = E; } -#define USB_LOG_TR(PREFIX, OP, ...) do { \ - int __E = USB.INTENB0.BRDYE; \ - USB.INTENB0.BRDYE = 0; \ - USB_LOG_TR(PREFIX, OP); \ - __VA_OPT__(USB_LOG(" " __VA_ARGS__)); \ - USB_LOG("\n"); \ - USB.INTENB0.BRDYE = __E; \ -} while(0) #else -#define USB_LOG_TR(PREFIX, OP, ...) +#define USB_LOG_TR(PREFIX, OP, FMT, ...) #endif -/* Unbind resources after a hardware segment is finished reading. */ -static void finish_read_hwseg(asyncio_op_t *t, int pipe) -{ -#ifdef GINT_USB_DEBUG - /* Log DTLN to help identify bugs where we clear data we didn't read */ - int DTLN = -1; - if(t->controller == CF) DTLN = USB.CFIFOCTR.DTLN; - if(t->controller == D0F) DTLN = USB.D0FIFOCTR.DTLN; - if(t->controller == D1F) DTLN = USB.D1FIFOCTR.DTLN; -#endif - - if(t->controller == CF) USB.CFIFOCTR.BCLR = 1; - if(t->controller == D0F) USB.D0FIFOCTR.BCLR = 1; - if(t->controller == D1F) USB.D1FIFOCTR.BCLR = 1; - - USB_LOG("frhwseg: DTLN=%d cont=%d interrupt=%d\n", DTLN, t->cont_r, - t->interrupt_r); - - asyncio_op_finish_read_hwseg(t); - USB_TRACE("finish_read_hwseg()"); - - if(!t->cont_r) { - fifo_unbind(t->controller); - t->controller = NOF; - } - - /* Re-enable communication for the next segment */ - USB_LOG_TR("frhwseg", t, "--> PID=BUF"); - USB.PIPECTR[pipe-1].PID = PID_BUF; -} - static void finish_read_round(asyncio_op_t *t, int pipe) { - USB_LOG("[PIPE%d] finished reading %d/%d bytes\n", pipe, - t->round_size, t->buffer_used); + USB_LOG("[PIPE%d] read %d/(r%d,b%d) bytes\n", pipe, + t->round_size, t->size, t->buffer_used); - bool hwseg_finished = asyncio_op_finish_read_round(t); - USB_LOG_TR("frr1", t, "hwseg finished = %d", hwseg_finished); - if(hwseg_finished) { - finish_read_hwseg(t, pipe); - handle_incoming_hwseg(t, pipe); + /* This call will propagate all changes to the op, including finishing + the hardware segment and call (if appropriate). The only thing it + doesn't do is invoke the callback, so we have a chance to switch to + PID=BUF before doing it manually */ + gint_call_t cb = t->callback; + int status = asyncio_op_finish_read_round(t); + + USB_LOG_TR("frr", t, "finished=%c%c%c", + status & ASYNCIO_HWSEG_EXHAUSTED ? 'H' : '-', + status & ASYNCIO_REQUEST_FINISHED ? 'R' : '-', + status & ASYNCIO_TRANSACTION_EXHAUSTED ? 'T' : '-'); + + if(status & ASYNCIO_HWSEG_EXHAUSTED) { +#ifdef GINT_USB_DEBUG + /* Log DTLN to help identify data loss bugs */ + int DTLN = -1; + if(t->controller == CF) DTLN = USB.CFIFOCTR.DTLN; + if(t->controller == D0F) DTLN = USB.D0FIFOCTR.DTLN; + if(t->controller == D1F) DTLN = USB.D1FIFOCTR.DTLN; +#endif + + if(t->controller == CF) USB.CFIFOCTR.BCLR = 1; + if(t->controller == D0F) USB.D0FIFOCTR.BCLR = 1; + if(t->controller == D1F) USB.D1FIFOCTR.BCLR = 1; + + USB_LOG("frr[seg]: DTLN=%d cont=%d interrupt=%d\n", + DTLN, t->cont_r, t->interrupt_r); + USB_TRACE("finish_read_round() [seg]"); + + if(status & ASYNCIO_TRANSACTION_EXHAUSTED) { + fifo_unbind(t->controller); + t->controller = NOF; + } + + /* Re-enable communication for the next segment */ + USB_LOG_TR("frr[seg]", t, "--> PID=BUF"); + USB.PIPECTR[pipe-1].PID = PID_BUF; } - - USB_LOG("req. progress: size=%d cont=%d interrupt=%d\n", - t->size, t->cont_r, t->interrupt_r); - - /* End the call if we exhausted either the buffer or the transaction */ - if(t->size == 0 || !t->cont_r) { - asyncio_op_finish_read(t); - USB_LOG_TR("fc", t); - USB_TRACE("finish_read_round() finish call"); + if(status & ASYNCIO_REQUEST_FINISHED) { + gint_call(cb); } } -static void read_round(asyncio_op_t *t, int pipe) +static bool read_round(asyncio_op_t *t, int pipe) { - int round_size = (t->size < t->buffer_used ? t->size : t->buffer_used); - asyncio_op_start_read_round(t, round_size); - USB_LOG_TR("rr", t); + int round_size = asyncio_op_start_read_round(t); + USB_LOG_TR("rr", t, ""); /* No data to read: finish the round immediately */ if(round_size == 0 || t->data_r == NULL) { finish_read_round(t, pipe); - return; + return true; } /* Read stuff (TODO: Smart reads + DMA) */ @@ -620,27 +624,39 @@ static void read_round(asyncio_op_t *t, int pipe) if(t->controller == D1F) FIFO = &USB.D1FIFO; void *dataptr = t->data_r; - for(int i = 0; i < round_size / 4; i++) { - *(uint32_t *)dataptr = *FIFO; - dataptr += 4; + if(dataptr) { + for(int i = 0; i < round_size / 4; i++) { + *(uint32_t *)dataptr = *FIFO; + dataptr += 4; + } + if(round_size & 2) { + *(uint16_t *)dataptr = *(uint16_t volatile *)FIFO; + dataptr += 2; + } + if(round_size & 1) { + *(uint8_t *)dataptr = *(uint8_t volatile *)FIFO; + dataptr += 1; + } } - if(round_size & 2) { - *(uint16_t *)dataptr = *(uint16_t volatile *)FIFO; - dataptr += 2; - } - if(round_size & 1) { - *(uint8_t *)dataptr = *(uint8_t volatile *)FIFO; - dataptr += 1; + else { + volatile int x; + for(int i = 0; i < round_size / 4; i++) + x = *FIFO; + if(round_size & 2) + x = *(uint16_t volatile *)FIFO; + if(round_size & 1) + x = *(uint8_t volatile *)FIFO; + (void)x; } finish_read_round(t, pipe); + return false; } static int handle_incoming_hwseg(asyncio_op_t *t, int pipe) { - /* Do nothing if no interrupt is waiting or if the pipe is already - processing a previous hardware segment */ - if(!t->interrupt_r || t->buffer_used > 0) + /* Do nothing if no interrupt is waiting or there is a hwseg */ + if(!t->interrupt_r || asyncio_op_has_read_hwseg(t)) return 0; /* PID will stay at NAK for the entire duration of the segment */ @@ -656,13 +672,6 @@ static int handle_incoming_hwseg(asyncio_op_t *t, int pipe) t->interrupt_r = false; - /* Continue an ongoing transfer */ - if(asyncio_op_busy(t)) { - USB_LOG("PIPE %d new hwseg while busy -> new round\n", pipe); - read_round(t, pipe); - return 0; - } - int data_available = 0; if(t->controller == CF) data_available = USB.CFIFOCTR.DTLN; if(t->controller == D0F) data_available = USB.D0FIFOCTR.DTLN; @@ -674,11 +683,21 @@ static int handle_incoming_hwseg(asyncio_op_t *t, int pipe) asyncio_op_start_read_hwseg(t, data_available, cont); USB_LOG_TR("nseg", t, "PIPECTR=%04x", USB.PIPECTR[pipe-1].word); + + /* Continue an ongoing transfer */ + if(asyncio_op_busy(t)) { + USB_LOG("PIPE %d new hwseg while busy -> new round\n", pipe); + read_round(t, pipe); + } + return 0; } -int usb_read_async(int pipe, void *data, int size, bool use_dma, - int *read_size, gint_call_t callback) +/* Performs a single asynchronous read. This function implements support for + the USE_DMA, IGNORE_ZEROS, and AUTOCLOSE flags. Returns a non-blocking error + code and invokes *callback after finite time. */ +static int read_once(int pipe, void *data, int size, int flags, int *rc_ptr, + gint_call_t const *cb) { asyncio_op_t *t = &pipe_transfers[pipe]; int rc; @@ -690,42 +709,91 @@ int usb_read_async(int pipe, void *data, int size, bool use_dma, if(t->type == ASYNCIO_NONE) return USB_READ_IDLE; - asyncio_op_start_read(t, data, size, use_dma, read_size, &callback); - read_round(t, pipe); - return 0; + /* Handle 0-byte reads immediately because size == 0 is a special case + for the asyncio structure (meaning no read request) */ + if(!size) { + if(rc_ptr) + *rc_ptr = 0; + gint_call(*cb); + return 0; + } + + bool USE_DMA = (flags & USB_READ_USE_DMA) != 0; + bool AUTOCLOSE = (flags & USB_READ_AUTOCLOSE) != 0; + bool IGNORE_ZEROS = (flags & USB_READ_IGNORE_ZEROS) != 0; + + asyncio_op_start_read(t, data, size, USE_DMA, rc_ptr, AUTOCLOSE, cb); + + /* Start the first round; others will follow from BRDY interrupts. When + dealing with a 0-byte read due to an exhausted transaction, this + function will finish the round immediately and return true. The user + callback will not be invoked since size > 0, so we can let the round + ru and emit USB_ERROR_ZERO_LENGTH afterwards. */ + bool zero_length = read_round(t, pipe); + + return (zero_length && IGNORE_ZEROS) ? USB_ERROR_ZERO_LENGTH : 0; +} + +/* Implements a generic sync or async read. This function implements support + for the WAIT and BLOCK flags, and the timeout. */ +int read_core(int pipe, void *data, int size, int flags, int *rc_ptr, + timeout_t const *timeout, gint_call_t cb) +{ + int volatile flag = 0; + int async_rc = 0; + if(flags & USB_READ_WAIT) { + cb = GINT_CALL_SET(&flag); + rc_ptr = &async_rc; + } + + /* Perform only a single read, unless USB_READ_BLOCK is set */ + while(1) { + int rc = read_once(pipe, data, size, flags, rc_ptr, &cb); + + /* On blocked timing errors, try again later */ + if((rc == USB_BUSY || rc == USB_READ_IDLE) + && (flags & USB_READ_BLOCK)) { + if(timeout_elapsed(timeout)) { + usb_read_cancel(pipe); + return USB_TIMEOUT; + } + sleep(); + continue; + } + /* On ignored zero-length reads, try again immediately */ + else if(rc == USB_ERROR_ZERO_LENGTH) + continue; + /* Other errors are fatal */ + else if(rc < 0 || !(flags & USB_READ_WAIT)) + return rc; + + /* Wait until the read completes */ + while(!flag) { + if(timeout_elapsed(timeout)) { + usb_read_cancel(pipe); + return USB_TIMEOUT; + } + sleep(); + } + return async_rc; + } +} + +int usb_read_async(int pipe, void *data, int size, int flags, int *rc_ptr, + timeout_t const *timeout, gint_call_t cb) +{ + return read_core(pipe, data, size, flags, rc_ptr, timeout, cb); } int usb_read_sync_timeout(int pipe, void *data, int size, bool dma, - timeout_t const *timeout) + timeout_t const *tm) { - int volatile flag = 0; - int read_size = 0; - - /* Wait until there is stuff to read, then read it */ - while(1) { - int rc = usb_read_async(pipe, data, size, dma, &read_size, - GINT_CALL_SET(&flag)); - if(rc == 0) - break; - if(rc != USB_BUSY && rc != USB_READ_IDLE) - return rc; - if(timeout_elapsed(timeout)) - return USB_TIMEOUT; - sleep(); - } - - /* Wait until the read completes */ - while(!flag) { - if(timeout_elapsed(timeout)) - return USB_TIMEOUT; - sleep(); - } - - /* Ignore 0-length reads */ - if(read_size == 0) - return usb_read_sync_timeout(pipe, data, size, dma, timeout); - - return read_size; + int flags = (dma ? USB_READ_USE_DMA : 0) + | USB_READ_IGNORE_ZEROS + | USB_READ_AUTOCLOSE + | USB_READ_WAIT + | USB_READ_BLOCK; + return read_core(pipe, data, size, flags, NULL, tm, GINT_CALL_NULL); } int usb_read_sync(int pipe, void *data, int size, bool use_dma) @@ -733,16 +801,13 @@ int usb_read_sync(int pipe, void *data, int size, bool use_dma) return usb_read_sync_timeout(pipe, data, size, use_dma, NULL); } -bool usb_poll(int pipe) +void usb_read_cancel(int pipe) { - if(pipe < 0) - return false; + if(pipe < 0 || pipe > 9) + return; - asyncio_op_t *t = &pipe_transfers[pipe]; - if(handle_incoming_hwseg(t, pipe)) - return false; - - return (t->type == ASYNCIO_READ) && (t->buffer_used > 0 || t->cont_r); + asyncio_op_cancel_read(&pipe_transfers[pipe]); + // TODO: usb_read_cancel: Also cancel DMA if it's running! } void usb_pipe_read_brdy(int pipe) @@ -752,9 +817,14 @@ void usb_pipe_read_brdy(int pipe) asyncio_op_t *t = &pipe_transfers[pipe]; - /* The BRDY interrupt also occurs after a pipe clear */ - if(!USB.PIPECTR[pipe-1].BSTS) + /* If a transfer is ongoing and stalled waiting for a new segment, + perform the round right now. This is acceptable because in that case + we are guarantees that a FIFO is bound is the round will succeed. */ + if(asyncio_op_has_read_call(t) && !asyncio_op_has_read_hwseg(t)) { + t->interrupt_r = true; + handle_incoming_hwseg(t, pipe); return; + } /* Signal the arrival to the main thread but don't do anything yet. This is both for proper error handling and because changing transfer @@ -767,5 +837,5 @@ void usb_pipe_read_brdy(int pipe) if(ep->intf->notify_read) ep->intf->notify_read(ep->dc->bEndpointAddress); - USB_LOG_TR("int", t); + USB_LOG_TR("int", t, ""); }