Skip to content

1) How UNFLoader works

Buu342 edited this page Nov 16, 2023 · 20 revisions

UNFLoader starts by detecting the connected flashcart and then assigns function pointers to functions related to said cart. These function pointers provide a way to abstract which flashcart the tool is connected to, and makes it easier for developers to extend the tool's functionality without needing to know how each cart works.

How UNFLoader abstracts the different flashcarts

UNFLoader provides the following functions (located in device.h) to abstract different processes of flashcart USB communication:

// Initializes the device library
void device_initialize();

// Finds what cart is connected via USB and initializes the function pointers
DeviceError device_find();

// Opens the USB pipeline
DeviceError device_open();

// Gets the maximum ROM size supported by the connected flashcart
uint32_t device_getmaxromsize();

// Checks if this flashcart requires ROMs to be padded to the nearest power of two
bool device_shouldpadrom();

// Checks if this flashcart requires CIC to be set explicitly before boot
bool device_explicitcic();

// Sends a ROM via USB
DeviceError device_sendrom(FILE* rom, uint32_t filesize);

// Checks if Debug Mode is supported by this flashcart
DeviceError device_testdebug();

// Uploads arbitrary data to the connected flashcart (More on this later)
DeviceError device_senddata(USBDataType datatype, byte* data, uint32_t size);

// Receives arbitrary data from the connected flashcart (More on this later)
DeviceError device_receivedata(uint32_t* dataheader, byte** buff);

// Checks if the USB pipeline is open
bool device_isopen();

// Closes the USB pipeline
DeviceError device_close();

ℹ️ The device.cpp + device.h files, and the subsequent cart specific device_x.cpp + device_x.h files are completely independent and can be taken wholesale and used in other C++ projects by you. Like this, you don't need to worry about writing your own implementation of the UNFLoader protocol.

Debug mode

If the -d argument is provided to UNFLoader during intialization, once a ROM is uploaded to a flashcart, UNFLoader enters debug mode. In this state, the tool enters a loop where it periodically calls the device_receivedata function to detect incoming data. This is handled by the debug_main function in debug.cpp. In order to access debug mode, device_testdebug must return DEVICEERR_OK. Whether it does will depend on the flashcart or potentially its firmware.

Communication protocol

How does the tool know what to do with the incoming data? How can it differentiate between prints and binary data? How does it know if the incoming data is part of a larger block? The answer is that UNFLoader uses a specific communication protocol to deal with these problems.

When uploading or receiving arbitrary data (Denoted hereafter as "data block"), UNFLoader expects it to be formatted like so:

  • 4 Bytes with 'D' 'M' 'A' '@' to signalize data start.
  • 4 Bytes with the data header (more on this in a bit).
  • N bytes with the data, where N is the size value provided in the data header.
  • Finally, 4 bytes with 'C' 'M' 'P' 'H' to signalize data end [1].

⚠️ When calling the device_senddata function in UNFLoader, it will automatically format the data to fit the communication protocol, the developer needn't worry about doing the formatting himself. Same applies for when calling usb_write or usb_read on the N64 side. The library will automatically format the data with this, and abstract it away from the developer. More on this (regarding the N64 library) in the next chapter.

The data header provides two important chunks of information. The first byte of the data header contains the type of data which is being sent/received, and the next 3 bytes state how large the incoming data is (Which can range between 1 byte to a maximum of 8MB). The data type mentioned in the first byte of the data header is up to the developer to implement, with the following data types already provided by default:

// Incoming data is text for printf
#define DATATYPE_TEXT       0x01

// Incoming data is raw binary
#define DATATYPE_RAWBINARY  0x02

// Incoming data describes contents of next incoming data
#define DATATYPE_HEADER     0x03

// Incoming data is a framebuffer
#define DATATYPE_SCREENSHOT 0x04

// Tells UNFLoader what version of the communication protocol the USB library is using
#define DATATYPE_HEARTBEAT  0x05

// Used for Remote Debugger Communication
#define DATATYPE_RDBPACKET  0x06

Clarification on data types below:

DATATYPE_HEADER

The DATATYPE_HEADER type exists to allow the N64 to describe the data it's about to send in the next USB data block in more detail. You can use this header data type to assist you when sending complex data through USB.

DATATYPE_SCREENSHOT

A framebuffer can be of an arbitrary size (such as 320x240 for NTSC and 320×288 for PAL) and bit depth (16 bits or 32 bits). Because of this, before sending a data block with DATATYPE_SCREENSHOT, a DATATYPE_HEADER data block is sent first, containing 16 bytes of data (4 with DATATYPE_SCREENSHOT so that we know the next incoming block is a framebuffer, 4 for the width in pixels, 4 for the height in pixels, and another 4 with the bit depth).

DATATYPE_RDBPACKET

Due to the small footprint of the debug library, RDB packets may sometimes not fit in a small buffer (for instance, if GDB requests all registers from the N64, a total of 72 64-bit registers needs to be sent (That's over a kilobyte of data)). As a result, an 8 bytes DATATYPE_HEADER packet is sent so that UNFLoader can combine packets before finally sending them over to the remote debugger. The first 4 bytes contain DATATYPE_RDBPACKET so that we know the incoming data is an RDB packet, and the last 4 bytes contain the number of DATATYPE_RDBPACKET chunks to buffer. If a DATATYPE_HEADER is not received beforehand, then it is safe to assume that all data is included and that the packet can be sent to the remote debugger.

DATATYPE_HEARTBEAT

As future proofing, this packet is sent so that, if there are significant communication protocol changes, UNFLoader can know which version of the data packet to expect. Currently, this data is 4 bytes:

  • 2 bytes which contain the USB protocol version
  • 2 bytes which contain the heartbeat version (so that the tool can handle changes to this packet's format in the future).

Any changes to the protocol are described in each flashcart's specific chapter.

DATATYPE_RDBPACKET

The incoming packet is meant to be sent to a separate remote debugger. Currently, UNFLoader only supports communication with GDB (and programs which utilize its protocol). RDB packets are not ready to be sent to GDB as is because they do not contain the +$ and #XX characters expected in GDB's Remote Serial Protocol. These are manually appended to the received data by UNFLoader before being sent to GDB.

Sending data in Debug Mode

The user can type text into the command prompt in debug mode. When ENTER is pressed, this data is sent to the N64 using DATATYPE_TEXT. This is called a "command".

When sending commands, the user can choose to wrap a filepath with '@' characters. This instead will cause UNFLoader to open the file wrapped in the delimiter characters and append it to the data it'll send. If the sent data only contains a filepath with '@' characters, then UNFLoader will simply send the contents of the file with DATATYPE_RAWBINARY. However, the user can choose to append data with some text before or after it, for instance: texture @file.bin@ 32. In this case, UNFLoader will use DATATYPE_TEXT but replace file.bin with the size of the file in bytes, and the contents of the file will be appended after the second '@'. So in that example, if file.bin contained abc, then the data portion of the sent data block will be texture @3@abc 32.

Data checksum

There is no checksum in place to detect the authenticity of the data. This might be implemented at a later date, but for now it is unused to speed up USB operations. A very simple method for a checksum would be to add every single byte of the received data together, and might be adopted in the future...


[1] While this is the usual format for packing data, the actual implementation on a per flashcart basis can change. Therefore you should look at each cart's specific page so that you can be aware of any potential caveats with the data packet.