Skip to content

2) How the USB Library works

Buu342 edited this page Nov 30, 2020 · 7 revisions

The USB library exists to allow communication between UNFLoader and the ROM running on the Nintendo 64. It too provides functions for reading and writing data from/to USB by abstracting away which cart the ROM is running on. The USB library is designed to be as raw as possible, abstracting away only what's necessary. As a result, a debug library is also supplied, which is a practical implementation of the USB library which properly deals with coordinating the data coming in and out, as well as providing some useful features like a fault handler.

How the USB library abstracts the different flashcarts

Just like UNFLoader, the USB library starts by detecting which cart is connected and then it proceeds to use function pointers to ensure that the correct code is ran for that device. It provides the following functions (located in usb.h) for communication:

// Initializes the USB buffers and pointers
char usb_initialize();

// Returns which flashcart is currently connected
char usb_getcart();

// Writes data to the USB.
void usb_write(int datatype, const void* data, int size);

// Returns the header of data being received via USB, or 0
u32 usb_poll();

// Reads bytes from USB into the provided buffer
void usb_read(void* buffer, int size);

// Skips a USB read by the specified amount of bytes
void usb_skip(int nbytes);

// Rewinds a USB read by the specified amount of bytes
void usb_rewind(int nbytes);

// Purges the incoming USB data
void usb_purge();

// Use these to conveniently read the data header from usb_poll()
#define USBHEADER_GETTYPE(header)
#define USBHEADER_GETSIZE(header)

Communication protocol

The communication protocol used by the USB library is exactly the same as UNFLoader's (obviously). Just like with UNFLoader, the developer needn't worry about creating the data headers/CMP signal and attaching it to the data, as the library will handle that for you. usb_poll will return 0 when there is no incoming data, and will otherwise return the data header (1 byte for the data type, and 3 bytes for the amount of data left to read). The USBHEADER_GETTYPE and USBHEADER_GETSIZE macros exist to simplify extracting this data from the return value of usb_poll.

How is data stored?

It was mentioned in the previous chapter that the USB library accepts up to 8MB of incoming USB data. How is this possible when the N64 only has 4-8MB of RAM? The answer is that the data is actually stored in ROM, because these flashcarts all have volatile ROM (otherwise, we wouldn't be able to upload via USB). The data is placed in the last megabytes of ROM, with a default size of 1MB (which can be increased/decreased by modifying usb.h). This means that, by default, your ROM can only be up to 63MB in size, as the last MB is reserved for the USB buffer. Increasing the size of the USB buffer means you get less ROM. This buffer is used for both USB reading and USB writing.

⚠️ The CMP signal and data header is not stored along with the data in ROM, therefore you do not need to worry about skipping 8 bytes when calling usb_read for the first time, nor about skipping the last 4 for the CMP signal.

When it comes to reading files appended in commands, the library does not abstract this, which means that you will read the '@' symbols and will need to deal with the data accordingly.

The usb_poll function doesn't actually just tell you if there's incoming data. Rather, it is the function that actually copies the incoming data from USB and writes it to ROM. usb_read doesn't actually read from USB, but rather from ROM where the data was placed (hence, why it is possible to skip or rewind).

Concurrency

It was mentioned that the USB buffers are used for both reading and writing, which brings up the problem of concurrency. If you're reading from the USB buffers and you want to write to USB, the bytes you are reading can get overwritten by what you're writing. As a result (and because some flashcarts cannot write to USB if there is data that needs to be read first), the library was designed so that it prioritizes reading from USB. This means, the usb_write function will silently fail unless usb_poll returns 0 (which means there is no data to read).

The debug library

The debug library abstracts the USB library further with a thread specific for USB I/O, and by providing a set of useful functions for parsing incoming commands. By default, the USB thread has a really high priority, and does not yield until it is finished processing. If you use the debug library, there is little need to use the USB library. The following functions are available:

// Initializes the debug and USB library.
void debug_initialize();

// Prints a formatted message to the developer's command prompt.
void debug_printf(const char* message, ...);

// Sends the currently displayed framebuffer through USB.
void debug_screenshot(int size, int w, int h);

// Halts the program if the expression fails.
#define debug_assert(expr)

// Check the USB for incoming commands.
void debug_pollcommands();

// Adds a command for the USB to read.
void debug_addcommand(char* command, char* description, char*(*execute)());

// Stores the next part of the incoming command into the provided buffer.
void debug_parsecommand(void* buffer);
    
// Returns the size of the data from this part of the command.
int debug_sizecommand();

// Prints a list of commands to the developer's command prompt.
void debug_printcommands();

Unlike the USB library, it isn't strictly required to call debug_pollcommands periodically to check for incoming data, as calling any other debug function will check for incoming USB data first and deal with it accordingly. However it is still recommended to do so as the USB pipeline will be blocked until that data is processed, and you will risk timing out UNFLoader if you take too long to read it.