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.
This commit is contained in:
Lephe 2023-03-18 19:09:35 +01:00
parent fc1f510288
commit 4983849510
Signed by untrusted user: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
6 changed files with 468 additions and 282 deletions

View File

@ -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 */

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,6 @@
#include <gint/drivers/asyncio.h>
#include <string.h>
#include <assert.h>
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;
}
}

View File

@ -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;
}
}

View File

@ -5,6 +5,7 @@
#include <gint/defs/util.h>
#include <string.h>
#include <stdio.h>
#include <gint/drivers/asyncio.h>
#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, "");
}