-
Notifications
You must be signed in to change notification settings - Fork 25
1) How UNFLoader works
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.
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.
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.
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].
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:
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.
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).
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.
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.
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.
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
.
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.