From fc1f51028884f74aa2760d1c7f8eac85ffce9486 Mon Sep 17 00:00:00 2001 From: Lephe Date: Sun, 12 Mar 2023 17:33:55 +0100 Subject: [PATCH] usb: consolidate reading mode (basic multi-segment reads now working) --- include/gint/drivers/asyncio.h | 176 +++++++++++-------- include/gint/usb-ff-bulk.h | 312 +++++++++++++++++---------------- include/gint/usb.h | 86 +++++++-- src/usb/asyncio.c | 99 ++++++----- src/usb/classes/ff-bulk.c | 149 ++++++++++++---- src/usb/pipes.c | 295 +++++++++++++++++++++---------- 6 files changed, 722 insertions(+), 395 deletions(-) diff --git a/include/gint/drivers/asyncio.h b/include/gint/drivers/asyncio.h index 601e716..d52aa7e 100644 --- a/include/gint/drivers/asyncio.h +++ b/include/gint/drivers/asyncio.h @@ -10,15 +10,22 @@ /* 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 fsync(2), and reads on a single data - transfer being made over several calls to read(2) (for asynchronous file - descriptors at least). - * Multi-round refers to the writes interacting multiple times with hardware - in order to communicate the complete data. + * Writes are multi-part because they are constructed over several calls to + write(2) followed by a "commit" with fynsc(2). They are additionally + multi-round because each call to write(2) requires mutiple rounds of + hardware communication when the hardware buffer is smaller than the data. + + * Reads are multi-part because each transaction from the host requires + several calls to read(2) ("user segments") if the user's buffer is shorter + than the full transaction. In addition, reads are multi-round because a + single read(2) to a user buffer takes multiple rounds of hardware + communication ("hardware segments") whenever the user buffer is larger + than the hardware buffer. The process of performing such an I/O operation, as tracked by this - structure and use throughout gint, is as follows. For a write: + structure and use throughout gint, is as follows. + + ## Writes WRITING ---------------------. ^ | | HW buffer @@ -68,17 +75,32 @@ FLYING-SYNC type == ASYNCIO_SYNC HW commit in progress ============================================================================ - For a read: + The series of asyncio_op function calls for a write is as follows: + + transaction ::= write* fsync + write ::= asyncio_op_start_write round+ asyncio_op_finish_write + round ::= asyncio_op_start_write_round asyncio_op_finish_write_round + fsync ::= asyncio_op_start_sync asyncio_op_finish_sync + + Each write(2) (with a single user-provided buffer) is split into multiple + rounds that each fill the (small) hardware buffer. More writes can follow + until an fynsc(2) commits the pipe. + + ## Reads + IN interrupt --> IDLE-EMPTY --------------> IDLE-READY - | \ | ^ - read(2) | \ Transaction read(2) | | Buffer full - | \ exhausted* | | + | \ read(2) | ^ + read(2) | \ Transaction | | User buffer + | \ exhausted* | | filled | '----<----------. | | | \ | | - v IN interrupt \ v | .---. Read from - WAITING ------------------> READING v hardware - '---' + | IN interrupt \ | | + v .--------->--------. \ v | .---. Read from + WAITING READING v hardware + '---------<--------' '---' + HW buffer exhausted with + user buffer not full 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. @@ -86,13 +108,17 @@ highlights 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 state returns to IDLE-EMPTY only if the transaction was exhausted while - the buffer is not full. This is to ensure that the caller can detect the end - of a data transfer by waiting for a read(2) which returns less bytes than - requested. In the case where a read(2) consumes exactly all remaining data, - we do not transition immediately to IDLE-EMPTY because we return as many - bytes as were requested. Instead, we return to IDLE-EMPTY after successfully - yielding 0 bytes in the next call. + A read(2) request (a "user segment") might consume several full hardware + buffers ("hardware segments") if the user buffer is large, thus looping + repeatedly between WAITING and READING. Conversely, each hardware segment + might fulfill many read(2) requests if the user buffer is small, thus + looping between IDLE-READY and READING. + + * Note that if the transaction finishes right as the user buffer fills up, + we return to IDLE-READY and the next call to read(2) will successfully read + 0 bytes and transition back to IDLE-EMPTY. This allows the user to read the + 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: @@ -100,13 +126,33 @@ ============================================================================ IDLE-EMPTY type == ASYNCIO_NONE No I/O operation ---------------------------------------------------------------------------- - IDLE-READY !data_r && buffer_used > 0 Hardware waiting for us to read + IDLE-READY !data_r Hardware segment pending ---------------------------------------------------------------------------- - WAITING data_r && !buffer_used Waiting for further HW data + WAITING data_r && !round_size User segment pending ---------------------------------------------------------------------------- - READING round_size > 0 DMA/CPU read from HW in progress + READING round_size > 0 DMA/CPU read round in progress ============================================================================ + The series of asyncio_op function calls for a read is a bit more complicated + because transactions are divided into two non-comparable sequences of + segments: one for packets received by the hardware buffer (on BRDY), one for + the data being copied to user buffers (on read(2)). + + |<------ Transaction from the host (no size limit) ------>| + + v BRDY v BRDY v BRDY v BRDY v BRDY + +-----------+-----------+-----------+-----------+---------+ + | HW buffer | HW buffer | HW buffer | HW buffer | (short) | HW segments + +-----------+------+----+-----------+-----------+---------+ + | 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 + read(2) user segments. + States can be checked and transitioned with the API functions below. */ enum { ASYNCIO_NONE, ASYNCIO_READ, ASYNCIO_WRITE, ASYNCIO_SYNC }; @@ -115,21 +161,23 @@ typedef volatile struct { /** User-facing information **/ - /* Type of I/O operation (read/write/fsync) */ - uint8_t type; - /* Whether the DMA should be used for hardware access */ - bool dma; - 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 */ + /* 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 **/ @@ -138,6 +186,12 @@ typedef volatile struct /* 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. */ @@ -168,52 +222,36 @@ void asyncio_op_clear(asyncio_op_t *op); bool asyncio_op_busy(asyncio_op_t const *op); //--- -// Operations and call functions -// -// Notice that for a write, the process is a single write call containing many -// write rounds and only a single finish_call(), whereas for a read the process -// is a single read reception contaning many read calls each with their own -// finish_call(), and only a single finish_read_reception(). +// I/O functions //--- -/* asyncio_op_start_write(): Start a write call */ -void asyncio_op_start_write(asyncio_op_t *op, void const *data, size_t size, - bool use_dma, gint_call_t const *callback); +/* Start/finish a write(2) call. */ +void asyncio_op_start_write(asyncio_op_t *op, + void const *data, size_t size, bool use_dma, gint_call_t const *callback); +void asyncio_op_finish_write(asyncio_op_t *op); -/* asyncio_op_start_sync(): Transition a write I/O operation to a fsync call */ -void asyncio_op_start_sync(asyncio_op_t *op, gint_call_t const *callback); - -/* asyncio_op_start_read(): Start a single-block read from hardware - Returns the size that will actually be read (may be smaller than `size`). */ -size_t asyncio_op_start_read(asyncio_op_t *op, void *data, size_t size, - bool use_dma, gint_call_t const *callback); - -/* asyncio_op_finish_call(): Update state after a read/write/fsync call - - This function should be called when the read(2)/write(2)/fsync(2) call last - started on the operation has concluded, including all of the hardware - effects. This isn't the moment when the syscall returns, rather it is the - moment when it completes its work. */ -void asyncio_op_finish_call(asyncio_op_t *op); - -//--- -// Write round functions -//--- - -/* asyncio_op_start_write_round(): Start a single-block write to hardware */ +/* Start/finish a single-block write to hardware. */ void asyncio_op_start_write_round(asyncio_op_t *op, size_t size); - -/* asyncio_op_finish_write_round(): Finish a write round and advance data */ void asyncio_op_finish_write_round(asyncio_op_t *op); -//--- -// Read group functions -//--- +/* Start an fsync(2) operation (after one or more writes) and finish it. */ +void asyncio_op_start_sync(asyncio_op_t *op, gint_call_t const *callback); +void asyncio_op_finish_sync(asyncio_op_t *op); -/* asyncio_op_start_read_group(): Start a read call */ -void asyncio_op_start_read_group(asyncio_op_t *op, size_t total_size); +/* Start/finish a read(2) call. */ +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); -/* asyncio_op_fininsh_read_group(): Reset operation after a read group */ -void asyncio_op_finish_read_group(asyncio_op_t *op); +/* Start/finish a hardware segment `cont` should be true if there will be + another segment in the same transaction. */ +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); #endif /* GINT_USB_ASYNCIO */ diff --git a/include/gint/usb-ff-bulk.h b/include/gint/usb-ff-bulk.h index 6aa614b..deb72d8 100644 --- a/include/gint/usb-ff-bulk.h +++ b/include/gint/usb-ff-bulk.h @@ -1,5 +1,27 @@ //--- -// gint:usb-ff-bulk - A trivial bulk-based transfer class +// gint:usb-ff-bulk - A bulk-based transfer class using the fxlink protocol +// +// This interface (class code 0xff/0x77) implements a simple bidirectional +// communication channel running the fxlink protocol. It basically talks to +// fxlink's interactive mode on the host, and can broadly be used for 4 things: +// +// 1. Sending standard messages to fxlink (screenshots, logs...). Convenient +// functions are provided in this header to minimize the overhead. fxlink +// running in interactive mode (fxlink -t) will handle these messages in a +// suitable way automatically (eg. save screenshots as PNG). +// +// 2. Sending custom messages to fxlink (bench results, memory dumps, crash +// reports...). fxlink running in interactive mode will forward the data to +// appropriate user-provided external programs to handle the messages. +// +// 3. Applications based around on data streams coming from the host (screen +// projector, web browser...). This can be achieved by modifying fxlink or +// using it as a library. +// +// 4. Remote control from the fxlink interactive mode (fxlink -t). This is +// based on fxlink's 'command' and 'keyboard' protocols. This module will +// automatically handle commands and respond to them as long as the message +// handling function is called regularly. //--- #ifndef GINT_USB_FF_BULK @@ -10,164 +32,37 @@ extern "C" { #endif #include +struct usb_fxlink_header; -/* The bulk transfer interface with class code 0xff provides a very simple - communication channel between the calculator and a host. There is - (currently) a single IN pipe that sends data from the calculator to the - host, at high-speed (USB 2.0). - - The class code of this interface is 0xff, which means that the protocol is - custom and requires a custom program on the host to receive the data (unlike - for instance LINK on a Graph 90+E which behaves as a USB stick and can be - used with the file browser). fxlink can be used to the effect. */ - +/* This bulk transfer interface with class code 0xff/0x77 implements a simple + bidirectional communication channel between the calculator and a host, + running at high-speed USB 2.0 and using the fxlink protocol. You can use it + to communicate with fxlink's interactive mode on the host. */ extern usb_interface_t const usb_ff_bulk; //--- -// Direct bulk access -// -// The following functions can be used to access the bulk pipes directly. They -// provide the pipe numbers, which can be passed for instance to usb_write -// functions. +// Sending standard messages //--- -/* usb_ff_bulk_output(): Pipe for calculator -> host communication */ -int usb_ff_bulk_output(void); - -//--- -// fxlink protocol -// -// fxlink is a bulk-based communication tool in the fxSDK that can be used to -// conveniently transfer or manipulate information during the execution of an -// add-in. For instance, screenshots can be saved, files can be transferred, -// and text can be logged. -// -// Each communication with fxlink is a message that consists of: -// * A first write with a message header; -// * One or more writes with message data (of a size announced in the header); -// * A commit on the USB pipe (to ensure that everything is sent). -// -// There are two ways to use the fxlink protocol in this header; you can either -// use one of the convenience functions like usb_fxlink_screenshot() that do -// all the steps for you, or you can perform the writes and commits yourself to -// send custom messages. -//--- - -/* usb_fxlink_header_t: Message header for fxlink - - fxlink supports a minimalist protocol to receive data sent from the - calculator and automatically process it (such as save it to file, convert - to an image, etc). It is designed as a convenience feature, and it can be - extended with custom types rather easily. - - A message is categorized with an (application, type) pair; both are UTF-8 - strings of up to 16 characters. The application name "fxlink" is reserved - for built-in types supported by fxlink; any other custom application name - can be set (in which case fxlink will call a user-provided program to handle - the message). - - The size of the data to be transferred must be specified in order of the - transfer to proceed correctly, as it cannot reliably be guessed on the other - side (and guessing wrong means all further messages will be in trouble). - - As with the rest of the USB protocol, all the multi-byte integer fields in - this header are encoded as *little-endian*. */ -typedef struct -{ - /* Protocol version = 0x00000100 */ - uint32_t version; - /* Size of the data to transfer (excluding this header) */ - uint32_t size; - /* Size of individual transfers (related to the size of the FIFO) */ - uint32_t transfer_size; - /* Application name, UTF-8 (might not be zero-terminated) */ - char application[16]; - /* Message type */ - char type[16]; - -} usb_fxlink_header_t; - -/* usb_fxlink_fill_header(): Fill an fxlink message header - - This function will fill the specified fxlink header. You need to specify the - exact amount of data that the fxlink message will contain; if you cannot - determine it, consider splitting the message into several messages each with - their own header, and use the application-specific script on the host to - recombine them. - - Returns false if the parameters are invalid or don't fit, in this case the - contents of the header are unchanged. */ -bool usb_fxlink_fill_header(usb_fxlink_header_t *header, - char const *application, char const *type, uint32_t data_size); - -//--- -// Short functions for fxlink built-in types -//--- - -/* Subheader for the fxlink built-in "image" type */ -typedef struct -{ - uint32_t width; - uint32_t height; - /* Pixel format, see below */ - int pixel_format; - -} usb_fxlink_image_t; - -/* Pixel formats */ -typedef enum -{ - /* Image is an array of *big-endian* uint16_t with RGB565 format */ - USB_FXLINK_IMAGE_RGB565 = 0, - /* Image is an array of bits in mono format */ - USB_FXLINK_IMAGE_MONO, - /* Image is two consecutive mono arrays, one for light, one for dark */ - USB_FXLINK_IMAGE_GRAY, - -} usb_fxlink_image_format_t; +/* usb_fxlink_text(): Send raw text + Send a string; fxlink will display it in the terminal. This can be used to + back stdout/stderr. Sending lots of small messages can be slow; if that's a + problem, fill in message manually. If size is 0, uses strlen(text). */ +void usb_fxlink_text(char const *text, int size); /* usb_fxlink_screenshot(): Take a screenshot - This function takes a screenshot. If (onscreen = false), it sends the - contents of the VRAM. This mode is best used just before dupdate(), since - the VRAM contents are exactly as they will appear on screen a moment later. - However, this is somewhat unintuitive for interactive GUI as the user has to - press a button after the option is present on-screen, therefore the contents - of the *next* frame are captured. + This function sends a copy of the VRAM to fxlink. This is best used just + before dupdate() since this ensures the image sent by USB is identical to + the one displayed on screen. - If (onscreen = true), this function tries to send the pixels that are - currently visible on-screen, with the following heuristic: - - * If there is only one VRAM, the VRAM contents are used in the hope they - haven't changed since the last frame was presented with dupdate(). - * If there are two VRAMs (on fx-CG 50 or using the gray engine) the contents - of the VRAM not currently being draw to are sent. - - This function does not read pixels directly from the display, as this is - usually slow and currently not even implemented. */ + If `onscreen` is set and there are two VRAMs (on fx-CG or when using the + gray engine on fx-9860G), sends a copy of the other VRAM. This is a bit more + intuitive when taking a screenshot of the last shown image as a result of a + key press. Note that this function never reads pixels directly from the + display (it's usually slow and currently not even implemented). */ void usb_fxlink_screenshot(bool onscreen); -#ifdef FX9860G -/* usb_fxlink_screenshot_gray(): Take a gray screenshot on fx-9860G - - This function is similar to usb_fxlink_screenshot(), but it takes a gray - screenshot. It depends on the gray engine so if you use your add-in will - automatically have the gray engine, that's why it's separate. */ -void usb_fxlink_screenshot_gray(bool onscreen); -#endif - -/* usb_fxlink_text(): Send raw text - - This function sends a string with the "text" type, which fxlink prints on - the terminal. It will send a full fxlink message (with a commit), which is - inefficient if there is a lot of text to send. For better speed, send the - message manually by filling the header and doing the writes and commit - manually. - - This function sends the text by blocks of 4 bytes or 2 bytes when alignment - and size allow, and 1 byte otherwise. If size is 0, strlen(text) is used. */ -void usb_fxlink_text(char const *text, int size); - /* usb_fxlink_videocapture(): Send a frame for a video recording This function is essentially the same as usb_fxlink_screenshot(). It sends a @@ -180,11 +75,130 @@ void usb_fxlink_text(char const *text, int size); void usb_fxlink_videocapture(bool onscreen); #ifdef FX9860G -/* usb_fxlink_videocapture_gray(): Send a gray frame for a video recording - Like usb_fxlink_videocapture(), but uses VRAM data from the gray engine. */ +/* Similar to usb_fxlink_screenshot(), but takes a gray screenshot if the gray + engine is currently running. */ +void usb_fxlink_screenshot_gray(bool onscreen); + +/* Like usb_fxlink_videocapture(), but uses VRAM data from the gray engine. */ void usb_fxlink_videocapture_gray(bool onscreen); #endif +//--- +// Receiving messages +//--- + +/* usb_fxlink_handle_messages(): Process and return incoming messages + + This function processes incoming messages. It handles some types of messages + internally (eg. fxlink commands) and returns the others to the caller by + loading the provided header structure and returning true. + + When this function returns true, the caller should read the message contents + on the interface's input pipe. Usually every read will return just a portion + of the message (eg. 2048 bytes) so the contents should be read with a loop. + + As long as this function returns true it should be called againt to process + any other messages. When there is no more messages to process, this function + returns false. It is important to call this function regularly from the main + thread when quick responses are expected. */ +bool usb_fxlink_handle_messages(struct usb_fxlink_header *header); + +/* usb_fxlink_drop_transaction(): Drop incoming data until end of transaction + + When a message arrives on the USB port incoming data *must* be processed in + order for the pipe to accept any further data in the future. This function + can be used in error situations to drop data until the end of the + transaction (which is usually the end of the message, at least with the + fxSDK's implementation of fxlink). */ +void usb_fxlink_drop_transaction(void); + +/* usb_fxlink_set_notifier(): Set up a notification for incoming messages + + Registers the provided function to be called when data arrives on the fxlink + input pipe. This signals that usb_fxlink_handle_messages() should be called + later from the main thread. Pass NULL to disable the notification. */ +void usb_fxlink_set_notifier(void (*notifier_function)(void)); + +//--- +// Direct bulk access +// +// The following functions can be used to access the bulk pipes directly. They +// provide the pipe numbers, which can be passed for instance to usb_write +// functions. +//--- + +/* 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); + +//--- +// Construction and analysis of fxlink protocol messages +// +// fxlink messages consist of a simple header followed by message contents +// (sometimes with a subheader). In addition to the functions above, it is +// possible to craft custom fxlink messages and send them manually. +// +// To send a message manually, simple write an fxlink header to the output +// pipe, followed by the contents. The message can be built from any number of +// writes to the pipe. After the last write, commit the pipe. +//--- + +/* usb_fxlink_header_t: Message header for fxlink + + Messages are categorized with an (application, type) pair; both are UTF-8 + strings of up to 16 bytes (NUL not required). The application name "fxlink" + is reserved for built-in types of messages. Other names can be used freely, + and fxlink's interactive mode on the host side will forward messages to + external programs based on the application name. + + The size of the data to be transferred must be specified upfront, but this + restriction might be lifted in the future. As with the rest of the USB + protocol, all the integer are encoded as *little-endian*. */ +typedef struct usb_fxlink_header +{ + /* Protocol version = 0x00000100 */ + uint32_t version; + /* Size of the data to transfer (excluding this header) */ + uint32_t size; + /* Size of individual transfers (related to the size of the FIFO) */ + uint32_t transfer_size; + /* Application name and message type, UTF-8 (need not be zero-terminated) */ + char application[16]; + char type[16]; + +} usb_fxlink_header_t; + +/* usb_fxlink_fill_header(): Fill an fxlink message header + + Fills an fxlink header's fields with the provided. You need to specify the + exact amount of data that the fxlink message will contain. Returns false if + parameters are invalid or don't fit the fields. */ +bool usb_fxlink_fill_header(usb_fxlink_header_t *header, + char const *application, char const *type, uint32_t data_size); + +/* Subheader for the fxlink built-in "image" type */ +typedef struct +{ + /* Image size; storage is row-major */ + uint32_t width; + uint32_t height; + /* Pixel format, see below */ + int pixel_format; + +} usb_fxlink_image_t; + +/* Pixel formats */ +enum { + /* Image is an array of *big-endian* uint16_t with RGB565 format */ + USB_FXLINK_IMAGE_RGB565 = 0, + /* Image is an array of bits in mono format */ + USB_FXLINK_IMAGE_MONO, + /* Image is two consecutive mono arrays, one for light, one for dark */ + USB_FXLINK_IMAGE_GRAY, +}; + #ifdef __cplusplus } #endif diff --git a/include/gint/usb.h b/include/gint/usb.h index 185a34a..89e68c7 100644 --- a/include/gint/usb.h +++ b/include/gint/usb.h @@ -137,8 +137,12 @@ struct usb_interface { /* Answer class-specific SETUP requests */ /* TODO */ - /* Notification that an endpoint has data read to be read */ - void (*notify_read)(int endpoint, int size); + /* Notification that an endpoint has data to be read. This function is + called frequently when data is being transmitted; the particular + timings depend on low-level details. The notification should be + passed down to cause the main thread to read later. Do not read in + the notification function! */ + void (*notify_read)(int endpoint); }; /* usb_interface_endpoint_t: Parameters for an interface endpoint @@ -269,24 +273,46 @@ 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`. This function will return as - soon as any data gets read, even if it's less than `size`. Thus, in order to - read all available data you should call this function in a loop until you - get less bytes than you requested. + and then reads up to `size` bytes into `data`. It synchronizes with USB + transactions on the pipe in the following ways: - It is possible for this function to return 0 bytes. If the data available on - the pipe was consumed by a previous read call which perfectly filled the - provided buffer, the pipe will still be considered in the "data available" - state with 0 bytes left to read. The next read call will then successfully - return 0 bytes. This makes it possible to consistently detect the end of a - transfer as the first read which returns less bytes than requested. + 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. + + 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: + + * 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. 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 full buffer. + 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); @@ -297,9 +323,18 @@ int usb_read_sync_timeout(int pipe, void *data, int size, bool use_dma, /* usb_read_async(): Asynchronously read from a USB pipe - This function is similar to usb_read_sync() except that it is non-blocking; - it returns USB_READ_INACTIVE if there is no active transfer on the pipe. It - will also read 0 bytes under the same conditions as usb_read_sync(). + This function is similar to usb_read_sync() except that it doesn't + synchronize with transactions, resulting in two differences: + + 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). + + 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. Being asynchronous, this function starts the read process and returns instantly; 0 on success, an error code otherwise. When the read finishes, @@ -308,6 +343,25 @@ int usb_read_sync_timeout(int pipe, void *data, int size, bool use_dma, int usb_read_async(int pipe, void *data, int size, bool use_dma, int *read_size, gint_call_t callback); +/* usb_poll(): Check whether there is data to read in a pipe + + 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: + + 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. + + 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 debugging functions //--- diff --git a/src/usb/asyncio.c b/src/usb/asyncio.c index 2f3953a..0dee29b 100644 --- a/src/usb/asyncio.c +++ b/src/usb/asyncio.c @@ -31,6 +31,18 @@ void asyncio_op_start_write(asyncio_op_t *op, void const *data, size_t size, op->callback = *callback; } +void asyncio_op_finish_write(asyncio_op_t *op) +{ + gint_call(op->callback); + + /* Keep relevant states until the transaction finishes with an fsync(2) */ + op->dma = false; + op->data_w = NULL; + op->size = 0; + op->callback = GINT_CALL_NULL; + op->round_size = 0; +} + void asyncio_op_start_write_round(asyncio_op_t *op, size_t size) { op->round_size = size; @@ -53,55 +65,64 @@ void asyncio_op_start_sync(asyncio_op_t *op, gint_call_t const *callback) op->callback = *callback; } -void asyncio_op_start_read_group(asyncio_op_t *op, size_t total_size) +void asyncio_op_finish_sync(asyncio_op_t *op) { - op->type = ASYNCIO_READ; - op->size = total_size; - /* These are specified differently on each read(2) call */ - op->dma = false; - op->data_r = NULL; - op->callback = GINT_CALL_NULL; - op->round_size = 0; + gint_call(op->callback); + asyncio_op_clear(op); } -size_t asyncio_op_start_read(asyncio_op_t *op, void *data, size_t size, - bool use_dma, gint_call_t const *callback) +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) { op->dma = use_dma; op->data_r = data; op->callback = *callback; - op->round_size = size; + op->realized_size_r = realized_size; + op->size = size; - return ((int)size < op->size ? (int)size : op->size); + if(realized_size) + *realized_size = 0; } -void asyncio_op_finish_read_group(asyncio_op_t *op) -{ - asyncio_op_clear(op); -} - -void asyncio_op_finish_call(asyncio_op_t *op) +void asyncio_op_finish_read(asyncio_op_t *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); - - /* Generally clean up the operation; for a write, keep relevant states - until the transaction finishes with an fsync(2); for a read, keep info - needed for further read(2) calls. */ - if(op->type == ASYNCIO_WRITE) { - op->dma = false; - op->data_w = NULL; - op->size = 0; - op->callback = GINT_CALL_NULL; - op->round_size = 0; - } - else if(op->type == ASYNCIO_READ) { - op->dma = false; - op->data_r = NULL; - op->size -= op->round_size; - op->callback = GINT_CALL_NULL; - op->round_size = 0; - } - else { - asyncio_op_clear(op); - } + op->dma = false; + op->data_r = NULL; + op->callback = GINT_CALL_NULL; + op->realized_size_r = NULL; + op->size = 0; +} + +void asyncio_op_start_read_hwseg(asyncio_op_t *op, size_t size, bool cont) +{ + op->type = ASYNCIO_READ; + op->buffer_used = size; + op->cont_r = cont; +} + +void asyncio_op_finish_read_hwseg(asyncio_op_t *op) +{ + if(!op->cont_r) + op->type = ASYNCIO_NONE; + op->buffer_used = 0; +} + +void asyncio_op_start_read_round(asyncio_op_t *op, size_t size) +{ + op->round_size = size; +} + +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; + op->size -= op->round_size; + op->round_size = 0; + + return (op->buffer_used == 0); } diff --git a/src/usb/classes/ff-bulk.c b/src/usb/classes/ff-bulk.c index 0bbc929..39a0555 100644 --- a/src/usb/classes/ff-bulk.c +++ b/src/usb/classes/ff-bulk.c @@ -1,10 +1,13 @@ #include #include #include +#include +#include #include #include +#include -static void notify_read(int endpoint, int size); +static void notify_read(int endpoint); static usb_dc_interface_t dc_interface = { .bLength = sizeof(usb_dc_interface_t), @@ -156,50 +159,134 @@ void usb_fxlink_videocapture(bool onscreen) capture_vram(onscreen, "video"); } +//--- +// Built-in command execution +//--- + +static char const * const str_MPU[] = { + [HWMPU_UNKNOWN] = "Unknown MPU", + [HWMPU_SH7337] = "SH7737", + [HWMPU_SH7305] = "SH7305", + [HWMPU_SH7355] = "SH7355", + [HWMPU_SH7724] = "SH7724", +}; +static char const * const str_CALC[] = { + [HWCALC_FX9860G_SH3] = "SH3-based fx-9860G-like", + [HWCALC_FX9860G_SH4] = "SH4-based fx-9860G-like", + [HWCALC_G35PE2] = "fx-9860G III/Graph 35+E II", + [HWCALC_PRIZM] = "Prizm fx-CG 10/20", + [HWCALC_FXCG50] = "fx-CG 50/Graph 90+E", + [HWCALC_FXCG_MANAGER] = "fx-CG Manager", + [HWCALC_FX9860G_SLIM] = "fx-9860G Slim", +}; + +static void execute_command(char const *cmd) +{ + if(!strncmp(cmd, "echo", 4)) { + char const *text = cmd+4 + strspn(cmd+4, " \t\n"); + usb_fxlink_text(text, 0); + } + if(!strncmp(cmd, "identify", 8)) { +#if defined(FX9860G) + char const *serial_number = (void *)0x8000ffd0; + char const *OS_version = (void *)0x80010020; + char const *BC_version = (void *)0x8000ffb0; +#elif defined(FXCG50) + char const *serial_number = (void *)0x8001ffd0; + char const *OS_version = (void *)0x80020020; + char const *BC_version = (void *)0x8001ffb0; +#endif + + char str[256]; + sprintf(str, "gint %.8s %s (0x%07x) on %s (%s) %.10s %.14s\n", + serial_number, + GINT_VERSION, GINT_HASH, + str_MPU[gint[HWMPU]], str_CALC[gint[HWCALC]], + OS_version, BC_version); + usb_fxlink_text(str, 0); + } +} + //--- // Data reception //--- -/* Copy of the header for received BULK messages */ -static usb_fxlink_header_t recv_header; -/* Size of transfer not yet read (implies valid header when non-zero) */ -static int recv_size = 0; +/* User notification function */ +static void (*recv_handler)(void) = NULL; -static void header_finished(void) +bool usb_fxlink_handle_messages(usb_fxlink_header_t *header_ptr) { - recv_header.version = le32toh(recv_header.version); - recv_header.size = le32toh(recv_header.size); - recv_header.transfer_size = le32toh(recv_header.transfer_size); + if(!usb_poll(usb_ff_bulk_input())) + return false; - int major = (recv_header.version >> 8) & 0xff; - int minor = (recv_header.version & 0xff); + /* Read a message header first */ + usb_fxlink_header_t header; + int rc = usb_read_sync(usb_ff_bulk_input(), &header, sizeof header, false); + + if(rc < 0) { + USB_LOG("[ff-bulk] Header read failed: %d\n", rc); + return false; + } + if(rc < (int)sizeof header) { + USB_LOG("[ff-bulk] Incomplete header (%d/%d bytes)\n", rc, + sizeof header); + return false; + } + + + header.version = le32toh(header.version); + header.size = le32toh(header.size); + header.transfer_size = le32toh(header.transfer_size); + + int major = (header.version >> 8) & 0xff; + int minor = (header.version & 0xff); + (void)minor; + (void)major; USB_LOG("[ff-bulk %d.%d] New %.16s.%.16s (%d bytes)\n", major, minor, - recv_header.application, recv_header.type, recv_header.size); + header.application, header.type, header.size); + + /* Handle built-in fxlink messages */ + if(!strncmp(header.application, "fxlink", 16) + && !strncmp(header.type, "command", 16)) { + USB_LOG("[ff-bulk] Receiving command (%d bytes)\n", header.size); + char *cmd = malloc(header.size + 1); + if(!cmd) + return false; + usb_read_sync(usb_ff_bulk_input(), cmd, header.size, false); + cmd[header.size] = 0; + USB_LOG("[ff-bulk] Command is: '%s'\n", cmd); + execute_command(cmd); + free(cmd); + return usb_fxlink_handle_messages(header_ptr); + } + + *header_ptr = header; + return true; } -static void notify_read(int endpoint, int size) +void usb_fxlink_set_notifier(void (*notifier_function)(void)) +{ + recv_handler = notifier_function; +} + +void usb_fxlink_drop_transaction(void) +{ + while(1) { + int rc = usb_read_sync(usb_ff_bulk_input(), NULL, 512, false); + /* Break on error or short read */ + if(rc != 512) + break; + } +} + +static void notify_read(int endpoint) { /* We only have one endpoint for reading, the bulk OUT */ (void)endpoint; - USB_LOG("[ff-bulk] Data available on %02x (%d bytes)\n", endpoint, size); +// USB_LOG("[ff-bulk] Data available on %02x\n", endpoint); - if(recv_size <= 0) { - usb_read_sync(usb_ff_bulk_input(), &recv_header, sizeof recv_header, - false); - header_finished(); - recv_size = recv_header.size; - } - else { - USB_LOG("-> malloc dropping\n"); - - char *data = malloc(size); - usb_read_sync(usb_ff_bulk_input(), data, size, false); - free(data); - - recv_size -= size; - if(recv_size < 0) - recv_size = 0; - } + if(recv_handler) + recv_handler(); } diff --git a/src/usb/pipes.c b/src/usb/pipes.c index d79e38c..22e5b17 100644 --- a/src/usb/pipes.c +++ b/src/usb/pipes.c @@ -159,10 +159,10 @@ static void fifo_bind(fifo_t ct, int pipe, int mode) usb_while(!USB.D1FIFOCTR.FRDY || USB.PIPECFG.DIR != mode); } - /* Enable USB comunication! */ - if(pipe == 0 && writing) + /* Enable USB comunication when writing */ + if(writing && pipe == 0) USB.DCPCTR.PID = PID_BUF; - if(pipe != 0) + if(writing && pipe != 0) USB.PIPECTR[pipe-1].PID = PID_BUF; } @@ -181,7 +181,7 @@ static void fifo_unbind(fifo_t ct) if(pipe <= 0) return; - /* Disbable communication on the pipe */ + /* Disable communication on the pipe */ USB.PIPECTR[pipe-1].PID = PID_NAK; usb_while(USB.PIPECTR[pipe-1].PBUSY); @@ -234,16 +234,16 @@ void usb_pipe_reset_fifos(void) Note that the "" event is asynchronous if the DMA is used. A full write will take zero or more complete rounds followed by zero or one - partial round before finish_call() is called: + partial round before finish_write_call() is called: - usb_write_async ::= complete_round* partial_round? finish_call + usb_write_async ::= complete_round* partial_round? finish_write_call And a commit will trigger a transmission of whatever is left in the buffer (including nothing) and wait for the BEMP interrupt. usb_commit_async ::= - finish_call + finish_write_call Most functions can execute either in the main thread or within an interrupt handler. */ @@ -281,36 +281,24 @@ static int pipe_bufsize(int pipe) return (USB.PIPEBUF.BUFSIZE + 1) * 64; } -/* This function is called when a read/write/fsync call and its associated - hardware interactions all complete. */ -static void finish_call(asyncio_op_t *t, int pipe) +/* Called when a write/fsync call and its hardware interactions all complete */ +static void finish_write_call(asyncio_op_t *t, int pipe) { - /* Unbind the USB controller used for the call, except for writes since - the USB module requires us to keep it until the final commit */ - if(t->type != ASYNCIO_WRITE && t->type != ASYNCIO_READ) { + /* Unbind the FIFO after a sync */ + if(t->type == ASYNCIO_SYNC) { fifo_unbind(t->controller); t->controller = NOF; } /* Disable interrupts */ - if((t->type == ASYNCIO_WRITE || t->type == ASYNCIO_SYNC) && pipe != 0) + if(pipe != 0) USB.BEMPENB &= ~(1 << pipe); - asyncio_op_finish_call(t); - USB_TRACE("finish_call()"); - - if(t->type == ASYNCIO_READ && t->size < 0) { - if(t->controller == CF) USB.CFIFOCTR.BCLR = 1; - if(t->controller == D0F) USB.D0FIFOCTR.BCLR = 1; - if(t->controller == D1F) USB.D1FIFOCTR.BCLR = 1; - - fifo_unbind(t->controller); - t->controller = NOF; - asyncio_op_finish_read_group(t); - USB_TRACE("finish_call() read group"); - - USB.PIPECTR[pipe-1].PID = PID_BUF; - } + if(t->type == ASYNCIO_WRITE) + asyncio_op_finish_write(t); + else if(t->type == ASYNCIO_SYNC) + asyncio_op_finish_sync(t); + USB_TRACE("finish_write_call()"); } /* This function is called when a round of writing has completed, including all @@ -330,7 +318,7 @@ static void finish_write_round(asyncio_op_t *t, int pipe) USB_TRACE("finish_write_round()"); if(t->size == 0) - finish_call(t, pipe); + finish_write_call(t, pipe); } /* write_round(): Write up to a FIFO's worth of data to a pipe @@ -463,19 +451,19 @@ int usb_commit_async(int pipe, gint_call_t callback) usb_pipe_flush4(t->shbuf, t->shbuf_size, FIFO); /* Switch from WRITE to SYNC type; this influences the BEMP handler and - the final finish_call() */ + the final finish_write_call() */ asyncio_op_start_sync(t, &callback); /* TODO: Figure out why previous attempts to use BEMP to finish commit TODO| calls on the DCP failed with a freeze */ if(pipe == 0) { USB.CFIFOCTR.BVAL = 1; - finish_call(t, pipe); + finish_write_call(t, pipe); return 0; } /* Set BVAL=1 and inform the BEMP handler of the commitment with the - SYNC type; the handler will invoke finish_call() */ + SYNC type; the handler will invoke finish_write_call() */ USB.BEMPENB |= (1 << pipe); if(t->controller == D0F) USB.D0FIFOCTR.BVAL = 1; if(t->controller == D1F) USB.D1FIFOCTR.BVAL = 1; @@ -522,7 +510,7 @@ void usb_pipe_write_bemp(int pipe) if(t->type == ASYNCIO_SYNC) { - finish_call(t, pipe); + finish_write_call(t, pipe); } else { @@ -532,26 +520,97 @@ void usb_pipe_write_bemp(int pipe) } } -int usb_read_async(int pipe, void *data, int size, bool use_dma, - int *read_size, gint_call_t callback) +//--- +// 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) { - asyncio_op_t *t = &pipe_transfers[pipe]; - if(asyncio_op_busy(t)) - return USB_BUSY; - if(t->type == ASYNCIO_NONE) - return USB_READ_IDLE; + USB_LOG("%s: %s buf=%d%s%s req=%d/%d", + prefix, + t->type == ASYNCIO_READ ? "READ" : "NONE", + t->buffer_used, t->cont_r ? "+":"", t->interrupt_r ? "!":"", + t->round_size, t->size); +} +#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, ...) +#endif - int actual_size = asyncio_op_start_read(t, data, size, use_dma, - &callback); - if(*read_size) - *read_size = actual_size; +/* 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 - USB_LOG("async read request for %d/%d bytes\n", size, t->size); + if(t->controller == CF) USB.CFIFOCTR.BCLR = 1; + if(t->controller == D0F) USB.D0FIFOCTR.BCLR = 1; + if(t->controller == D1F) USB.D1FIFOCTR.BCLR = 1; - /* No data to read: finish the call immediately */ - if(actual_size == 0) { - finish_call(t, pipe); - return 0; + 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); + + 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); + } + + 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"); + } +} + +static void 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); + + /* No data to read: finish the round immediately */ + if(round_size == 0 || t->data_r == NULL) { + finish_read_round(t, pipe); + return; } /* Read stuff (TODO: Smart reads + DMA) */ @@ -560,34 +619,91 @@ int usb_read_async(int pipe, void *data, int size, bool use_dma, if(t->controller == D0F) FIFO = &USB.D0FIFO; if(t->controller == D1F) FIFO = &USB.D1FIFO; - void *dataptr = data; - for(int i = 0; i < size / 4; i++) { + void *dataptr = t->data_r; + for(int i = 0; i < round_size / 4; i++) { *(uint32_t *)dataptr = *FIFO; dataptr += 4; } - if(size & 2) { + if(round_size & 2) { *(uint16_t *)dataptr = *(uint16_t volatile *)FIFO; dataptr += 2; } - if(size & 1) { + if(round_size & 1) { *(uint8_t *)dataptr = *(uint8_t volatile *)FIFO; dataptr += 1; } - finish_call(t, pipe); + finish_read_round(t, pipe); +} + +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) + return 0; + + /* PID will stay at NAK for the entire duration of the segment */ + USB.PIPECTR[pipe-1].PID = PID_NAK; + + if(t->controller == NOF) { + fifo_t ct = fifo_find_available_controller(pipe); + if(ct == NOF) + return USB_READ_NOFIFO; + fifo_bind(ct, pipe, FIFO_READ); + t->controller = ct; + } + + 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; + if(t->controller == D1F) data_available = USB.D1FIFOCTR.DTLN; + + /* USB requires a zero-length or short packet to finish a transaction, + which equates to a partially-full buffer */ + bool cont = (data_available == pipe_bufsize(pipe)); + + asyncio_op_start_read_hwseg(t, data_available, cont); + USB_LOG_TR("nseg", t, "PIPECTR=%04x", USB.PIPECTR[pipe-1].word); return 0; } -int usb_read_sync_timeout(int pipe, void *data, int size, bool use_dma, +int usb_read_async(int pipe, void *data, int size, bool use_dma, + int *read_size, gint_call_t callback) +{ + asyncio_op_t *t = &pipe_transfers[pipe]; + int rc; + + if((rc = handle_incoming_hwseg(t, pipe))) + return rc; + if(asyncio_op_busy(t)) + return USB_BUSY; + 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; +} + +int usb_read_sync_timeout(int pipe, void *data, int size, bool dma, timeout_t const *timeout) { 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, use_dma, &read_size, + while(1) { + int rc = usb_read_async(pipe, data, size, dma, &read_size, GINT_CALL_SET(&flag)); if(rc == 0) break; @@ -599,19 +715,15 @@ int usb_read_sync_timeout(int pipe, void *data, int size, bool use_dma, } /* Wait until the read completes */ - while(!flag) - { + while(!flag) { if(timeout_elapsed(timeout)) return USB_TIMEOUT; sleep(); } - /* Finish the read if there are 0 bytes left to read */ - asyncio_op_t *t = &pipe_transfers[pipe]; - if(t->size == 0) { - t->size = -1; - finish_call(t, pipe); - } + /* Ignore 0-length reads */ + if(read_size == 0) + return usb_read_sync_timeout(pipe, data, size, dma, timeout); return read_size; } @@ -621,38 +733,39 @@ 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) +{ + if(pipe < 0) + return false; + + 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); +} + void usb_pipe_read_brdy(int pipe) { - asyncio_op_t *t = &pipe_transfers[pipe]; - if(asyncio_op_busy(t)) - USB_LOG("pipe %d BRDY while busy?!\n", pipe); - USB_LOG("[PIPE%d] BRDY with PIPECTR %04x\n", pipe, USB.PIPECTR[pipe-1].word); + + asyncio_op_t *t = &pipe_transfers[pipe]; + + /* The BRDY interrupt also occurs after a pipe clear */ if(!USB.PIPECTR[pipe-1].BSTS) return; - /* Prepare a FIFO and the transfer structure so the read can be done - TODO: FIXME: Prevent race accesses during USB interrupts */ - if(t->controller != NOF) - USB_LOG("pipe %d BRDY bound while not busy?!\n", pipe); - else { - fifo_t ct = fifo_find_available_controller(pipe); - /* TODO: BRDY: Delay FIFO binding until read syscall */ - if(ct == NOF) - return; - fifo_bind(ct, pipe, FIFO_READ); - t->controller = ct; - } - - int data_available = 0; - if(t->controller == CF) data_available = USB.CFIFOCTR.DTLN; - if(t->controller == D0F) data_available = USB.D0FIFOCTR.DTLN; - if(t->controller == D1F) data_available = USB.D1FIFOCTR.DTLN; - - asyncio_op_start_read_group(t, data_available); + /* Signal the arrival to the main thread but don't do anything yet. + This is both for proper error handling and because changing transfer + data asynchronously would require disabling the interrupt in other + sections of the driver, which seems to cause data loss. */ + t->interrupt_r = true; /* Notify the interface so it can read or schedule to read */ endpoint_t *ep = usb_get_endpoint_by_pipe(pipe); - ep->intf->notify_read(ep->dc->bEndpointAddress, data_available); + if(ep->intf->notify_read) + ep->intf->notify_read(ep->dc->bEndpointAddress); + + USB_LOG_TR("int", t); }