Mario Kart Live: Home Circuit

This page documents the Mario Kart Live: Home Circuit game.

Communication with the kart is done directly over local-WLAN via an access point that the game sets up using lp2p:app, making it the first title on retail to use lp2p. The underlying device management uses RCD. The RCD implementation is in the main-codebin itself, without symbols - however there are strings for this.

This is also the first known title on retail which uses stack cookies. This is used by main-codebin, the ssp functionality in sdknso is still not used other than being called from an initialization func. This is implemented in the main-codebin as follows:

  • The global u64 __stack_chk_guard is loaded then saved immediately before {first saved register} on stack, during func entry. During func exit, the global u64 is compared with the cookie on stack, it will call __stack_chk_fail on mismatch. __stack_chk_fail just executes an undefined instruction to trigger a crash.
  • There is no initialization func for __stack_chk_guard, it's just a hard-coded constant: 0xDEADBEEFDEADBEEF. Since it's constant, this renders the stack cookie useless.

RomFs contains only two files:

  • "data.zip"
  • "update.pua": This is the firmware update data for the Kart. This is a tar archive. The extracted archive contains "update.pui" and "pui.hash". The latter is a binary 0x100-byte file. The former is another tar archive, the content of that archive is the following:
    • "config.txt": Contains config which includes fields for efuse_key, efuse_fw, secure_boot, etc. Also references the data under generic/. Seems to be configuration for firmware installation, not uboot.
    • "audiofw_sha": 0x20-byte binary SHA256 hash for the "bluecore.audio.aes" file.
    • "dtb_sha": 0x20-byte binary SHA256 hash for the .dtb file.
    • "rootfs_sha": 0x20-byte binary SHA256 hash for the "root.nand.cpio.gz_pad.img.aes" file.
    • "tee_sha": 0x20-byte binary SHA256 hash for the tee file.
    • "uImage_sha": 0x20-byte binary SHA256 hash for the "nand.uImage.aes" file.
    • "generic/": This contains:
      • "android.nand.dtb": Plaintext "kernelDT".
      • "bluecore.audio.aes": Encrypted "audioKernel".
      • "nand.uImage.aes": Encrypted "linuxKernel".
      • "root.nand.cpio.gz_pad.img.aes": Encrypted "InitrdRootFS".
      • "tee.bin.aes": Encrypted "tee".

Note that the only firmware archive files accessed by the game are "update.pui", "pui.hash", and "config.txt". The content of "config.txt" is only used with sscanf() to extract the version fields. "update.pui"/"pui.hash" are probably sent over the network connection to the kart - it's unknown whether the game does anything with the content of "pui.hash" other than this.

Kart

The kart is likely codenamed "Fuji" as this is both what the game calls it internally, and how it identifies itself during RCD handshake. Various strings in the kart OSS refer to it as "DHC".

OSS is available for the kart itself.

This uses Linux. The 1.1.0_3 archive contains the following:

 busybox-1.22.1.tar.bz2
 eudev-1.5.3.tar.gz
 kmod-17.tar.xz
 libnl-3.2.24.tar.gz
 linux-kernel_5c3cb2e0be2243f6d4553ccad2047c9d72e25ea2.tar.gz
 lrzsz-0.12.20.tar.gz
 PsdDriver_5a8d821.zip
 rtl8188eu_074cc66fece232b0d5f1e1f7de57e72022ec12b1.tar.gz
 uboot_53a0fa98b176329e340b0a2fca6edb7117209751.tar.gz
 util-linux-2.24.2.tar.xz

PsdDriver is Nintendo's custom kernel module, the GPL license header used in the source starts with the following:

 * Sensors and Motors driver
 * Copyright (C) 2020 Nintendo Co, Ltd

The only changes in the OSS for 1.0.0_1 -> 1.1.0_3 are the following (note that there are more versions between these):

  • The following archives were updated: linux-kernel, PsdDriver, rtl8188eu, uboot.
  • In the PsdDriver source, the line-ending at the start of various source files was updated.
    • In sources/psd_util.c, initialize_table(); is now called by a dedicated psd_util_init_crc8 function instead of psd_util_get_crc8, which is now called by device_init in sources/psd.c.

The above git-commit-hashes (?) from the filenames doesn't seem to match commits in the upstream repos.

On October 28, 2020, the existing OSS archives were updated without adding a new version. With 1.1.0_3, the uboot archive (which has the same filename) had the "/examples" directory removed.

Hardware

Component Manufacturer Part Notes
SoC Realtek RTD1195 (PB) Contains hardware H.264 encoder
DDR3 RAM Winbond W631GG6MB-15 128 MiB
NAND Flash Winbond W29N01HVSINA 128 MiB
WNIC Realtek RTL8188E Connected to SoC via USB
MCU STMicroelectronics STM32F Responsible for real-time steering and throttle control loops, six-axis IMU acquisition, GPIO, battery status
IMU STMicroelectronics LSM6DSL Unconfirmed. Mounted "upside-down" on the PCB: +X=right +Y=back +Z=down. Sensitivities: 4.375 m°/s, 0.244 mg.
Li-ion pouch cell Nintendo branded; OEM unknown HAC-038 3.7V nominal; 1750 mAh capacity
Camera Unknown Unknown Possibly connected via MIPI CSI

Pairing process

The kart communicates with the Switch via standard 802.11 frames on channel 1/6/11, with standard CCMP encryption, but the authentication and key exchange protocol is Nintendo-proprietary. See LDN services for more information.

On first startup, Home Circuit generates a random SSID (beginning with 'G') and 0x20-byte PSK (for use in the aforementioned key exchange; it is not standard WPA). These are saved and reused on every subsequent startup, so that any kart(s) with this information stored can reconnect without needing to be paired again.

When pairing, Home Circuit creates a temporary network (random SSID beginning with 'P', randomized PSK, neither stored) and shows the details for this "pairing network" in a QR code for the kart to scan. Once the kart connects, it fetches the main "game" network SSID+PSK and stores them, and both it and Home Circuit exit pairing mode. The pairing network is only active while the QR code is displayed, otherwise the main "game" network runs.

The QR code is a "version 4" (33x33) code with level-M error correction. It uses byte encoding, and stores 0x3E bytes:

Offset Size Description
0x0 0x10 Pairing seed. LP2P PSK is SHA256(seed)
0x10 0x20 Pairing SSID. Remaining space is filled with zeros.
0x30 0x2 Pairing channel. Encoded little-endian. Usually 0.
0x32 0xC Padding bytes; zero.

Network protocols

Once the kart connects to the "game" network hosted by Home Circuit, it requests an IP address via DHCP, then connects to the Switch via standard TCP/IP to announce its presence. A series of TCP and UDP connections are established to exchange information, stream video, transmit control signals, monitor kart telemetry, etc.

Summary of ports
Protocol Endpoint Port Description RCD service ID
Management
TCP Switch 5201 RCD handshake service (pairing only) 0x0001
5202 RCD handshake service (non-pairing only) 0x0001
Kart 5103 "Fuji Control" RCD service 0x0100
5106 "Fuji Pairing" RCD service (pairing only) 0x0102
5107 Unknown ("Event"?) RCD service 0x0103
UDP Kart 5004 Time synchronization N/A
Video
UDP Switch 5016+kartid "LSP" video streaming N/A
TCP 5032+kartid "LSP" control channel N/A (non-RCD)
Control
UDP Kart 5102 Teleoperation (throttle, steering, tail light control) N/A
Switch 5116+kartid Telemetry N/A

Note: The ports determined by the kart ID are actually assigned by the Switch and configured via the "Fuji Control" service. The kart itself will actually use any port it is told to use; shown above are how Home Circuit calculates the port numbers.

Handshaking

After the kart connects to the LP2P network and requests an IP address via DHCP, it connects to the RCD handshake service. The port it uses is determined by whether it is in pairing mode (i.e. it learned the network details from a QR code) or not. When pairing, it connects to port 5201, otherwise it uses port 5202. The RCD handshake is the same in either case.

Upon a successful RCD handshake, the handshake channel is left open. The kart will then open ports 5103 and 5107 (or, when pairing, only port 5106) and expect a single connection on each: The listening socket is closed once connection(s) are established to prevent multiple connections, and if connection(s) aren't made within 5 seconds of a completed handshake, the kart will reset its network connection (it closes all TCP connections, releases its IP back to DHCP, disassociates the wireless link, and starts the connection process anew). It will also reset the network connection in this manner if any RCD connection is lost/closed.

"Fuji Pairing" RCD service

The pairing service implements a single command, and is only available immediately after a handshake and when pairing. A network reset will occur if the connection is idle for 1 second.

Command 0x01: SetGroupInfo

Accepts a 0x40-byte payload:

Offset Size Description
0x00 0x20 SSID, zero-terminated. Home Circuit copies this straight from GroupInfo, so uninitialized bytes may follow.
0x20 0x20 LP2P PSK

On success, responds with an empty payload. The kart commits these network settings to non-volatile memory immediately, replacing the old settings (if present). A network reset is initiated, and the kart exits pairing mode, using these newly-provided network settings after the reset. The Switch also exits pairing mode to accept the newly-paired kart.

"Fuji Control" RCD service

This service is used to set up and manage the kart. It is only available when not in pairing mode.

Command 0x01: GetSystemInfo

Takes no payload as input, returns:

Offset Size Description
0x0 0x1 boot_major_version
0x1 0x1 boot_minor_version
0x2 0x1 system_major_version
0x3 0x1 system_minor_version
0x4 0x29 Zero-terminated ASCII string which is a 160-bit value in hexadecimal. Appears to be some kind of SHA1.

Command 0x02: SetParam

Returns no output (on success) or error code 0x1060e8 if the parameter isn't recognized. Input:

Offset Size Description
0x0 0x80 Parameter name, zero-padded.
0x80 0x2 Value length
0x82 0xE Zero padding to align to 0x10-byte boundary
0x90 Varies Value to set

Setting the parameter "connection_info" is required to prepare the kart to drive. It can only be set once (trying to set it again gives error 0x1040e8). It expects (all values little-endian):

Offset Size Description
0x0 0x2 Fixed 0x0001; unknown purpose.
0x2 0x2 Telemetry (UDP) port on Switch
0x4 0x2 "LSP" control (TCP) port on Switch
0x6 0x2 "LSP" video (UDP) port on Switch
0x8 0x8 Current network time, per nn::time::StandardNetworkSystemClock::GetCurrentTime. (A Unix timestamp with one-second precision.)

Command 0x03: GetParam

Returns the value of the parameter (on success) or error code 0x1060e8. Expected input:

Offset Size Description
0x00 0x80 Parameter name, zero-padded.

One of the known parameters is "product_code" which includes the kart's character as 1 byte, and includes a string with the value of the kart's serial number on the bottom of the kart (white-label barcode).

Offset Size Description
0x00 0x80 Fixed 0x010000; unknown purpose
0x3 0x1 Fixed 0x03; unknown purpose
0x7 0x1 Fixed 0x24; unknown purpose
0xC 0x1 Fixed 0x01; unknown purpose
0x11 0x1 Fixed 0x14; unknown purpose
0x20 0x2 Fixed 0x0100; padding
0x22 0x1 Character ID; 0x01 = Mario, 0x02 = Luigi, unknown if there are other "hidden" characters
0x23 0x2 Fixed 0x0001; padding
0x24 0xF Kart Serial Number;

X: Nintendo Switch Platform

Q: Hardware Identifier for the Mario Kart Live: Home Circuit kart.

J/W: Factory Location; Either W: US, or J: Japan

10/14/40/70: Factory Number

Last 9 Numbers: Unit Number

Command 0x04: SetState

This sets the kart between drive mode and "parked" mode. "connection_info" must be set first; it will return error 0x1040e8 if not. Returns an empty payload on success. It expects:

Offset Size Description
0x0 0x1 Drive mode. 0x01 enables driving controls (and video), 0x00 puts the kart to "sleep."
0x1 0xF Zero padding.

Command 0x09: Shutdown

Takes no input and gives no output.

Disables the "network reset" behavior so that the kart will power off when the RCD channel(s) are closed.

Command 0x11: StartFluorescentLightDetection

TBD

Command 0x12: ReadApplicationData

Expects 0x20-byte zero-padded application ID (Home Circuit uses "YVCOQ00000000XFB") and retrieves data stored in the kart unique to the application.

Teleoperation

The kart is operated over UDP port 5102. This port is only open when the kart is in drive state. It expects packets of the form:

Offset Size Description
0x0 0x1 Throttle; a signed integer. -128 is full-speed reverse, +127 is full-speed forward.
0x1 0x1 Steering; a signed integer. -128 is full left, +127 is full right.
0x2 0x1 Brake light control. 0x00 turns the brake light off, 0x01 turns it on. This is independent of throttle.
0x3 0x1 Pad byte, apparently always zero.
0x4 0x4? Little-endian packet counter. Incremented by 1 for each sent packet. Unknown total size.
0x8? 0x18? Zero padding to bring the total packet size to 0x20.

Home Circuit sends these packets at 30 Hz. Any rate that is at least 1 Hz will do, however. The kart will automatically zero the throttle (and only the throttle) if a valid packet is not received in a one-second period, as protection from loss of control.

The counter appears to be intended as a mechanism for detecting duplicated/reordered packets, as such packets will have a counter that is not greater than the previously-accepted packet and can be quickly discarded. However, it appears to be ignored entirely by the kart. Nevertheless, it is a good idea to include it anyway.

NOTICE: The throttle/steering values take effect immediately. Home Circuit applies smoothing to these values, possibly to simulate the physics of a much more massive kart. However, this smoothing may also be necessary to keep the kart from suffering undue mechanical stress.

NOTICE: Home Circuit contains logic to detect when the kart is being held, and it locks out the controls in such cases. This logic is not implemented in the kart, which will eagerly follow any valid throttle/steering command given. The kart is not likely to cause injury if operated while held, but nevertheless, if you do so, it is at your own risk.

Telemetry

The kart will send UDP packets to the Switch over a port it specifies in "connection_info" (but typically port 5116) indicating its current status. The packet type is indicated by the first byte. (The tables below ignore this first byte.)

Type 0x01: Battery Status

Offset Size Description
0x0 0x1 Cable status bitmask: 0x1 = Cable connected; 0x2 = Unknown; 0x4 = Unknown
0x1 0x2 Zero padding
0x3 0x1 How many bars to show in the HUD for battery status (0x00-0x04)
0x4 0x1C Zero padding, to bring total packet length to 0x20.

One would think this would be a good location for wireless metrics (e.g. RSSI), but this is presumably measured only on the Switch side of the link.

Type 0x02: IMU

The kart contains a 6DoF IMU for measuring movement. This packet type communicates the motion status.

All integers are little-endian:

Offset Size Description
0x0 0x2 Total IMU samples, as an unsigned integer. This number includes the samples in this packet. It overflows.
0x2 0x1 Flags bitfield: IGA??NNN. I/G/A=High-precision units used, ?=Unknown, possibly clipping indicators. N=Number of IMU samples.
0x3 0x1 Pad byte; zero.
0x4 0x4 Microsecond-precision timer, as an unsigned integer.
0x8 0x10 Orientation: s32 qi, qj, qk, qr; gives the coefficients of a quaternion. Units appear to be increments of 2^-30, but quaternions should always be normalized before use.
0x18 0xC Integrator A position s32 x, y, z; gives an estimate of total displacement since this integrator was reset. Every 6ms step: pos += vel>>12; Units: (4096*0.006)² * 2^-30 m (I=1), 78.4348 * (4096*0.006)² * 2^-30 m (I=0)
0x24 0xC Integrator B position
0x30 0xC Integrator A velocity s32 x, y, z; gives an estimate of total change in velocity since this integrator was reset. Every 6ms step: vel += acc>>12; (where 'acc' is acceleration in 2^-30 m/s²). Units: (4096*0.006) * 2^-30 m/s (I=1), 78.4348 * (4096*0.006) * 2^-30 m/s (I=0)
0x3C 0xC Integrator B velocity
0x48 0xC*(NNN+1) 1-8 raw IMU samples s16 accel_x, accel_y, accel_z; // Units: 9.81/4096 m/s² (A=1), 15.99*9.81/4096 m/s² (A=0) (9.81m/s² added to accel_z to compensate for gravity)
s16 ang_vel_x, ang_vel_y, ang_vel_z; // Units: 0.004375 °/s (G=1), 0.1 °/s (G=0)

The exact rate at which these packets are sent appears to be somewhat variable. The raw IMU samples seem to be collected with a period of 6ms, and when not in drive mode, this packet always contains 8 IMU samples, for an overall period 48ms. However, when in drive mode, not all of these packets contain exactly 8 IMU samples, and the packets will arrive at an uneven rate. This is probably due to some logic in the kart that flushes the buffer early based on certain conditions.

The IMU itself is mounted on the PCB "upside down" which gives a reference frame different from what one would expect: While +X does correspond to the driver's right-hand side, the +Y vector extends beyond the rear of the vehicle, and +Z extends out the bottom. All measurements are taken with respect to this reference frame.

The orientation quaternion is reasonably stable, but should still only be trusted for relative measurements over short periods of time, as the quaternion can drift due to error (noise, clipping, quantization) in the IMU samples. The quaternion is initialized to 0i+0j+0k+1 once the IMU is enabled (i.e. when "connection_info" is set).

The integrators are useful in an environment where packet loss is expected to be reasonably high, and so not all raw IMU samples can be reliably collected. The integrators are periodically reset (every 0x2000 samples), as they will accumulate error over time.

Both integrators are reset at the same time when the IMU is enabled, but integrator A is reset at sample 0x1000 so that the resets are staggered.

Type 0x03: Motion feedback

TBD

Versions

This section documents the changes for game-updates.

v1.0.1

This only updated the Kart firmware.

ExeFs:

  • Nothing changed besides the usual NPDM update.

RomFs:

  • Only update.pua was updated, the following was changed in the extracted update.pui (pui.hash in update.pua was also updated):
    • "config.txt": system_minor_version was changed from "3" to "4".
    • "audiofw_sha"
    • "rootfs_sha"
    • "uImage_sha"
    • "generic/bluecore.audio.aes": Starts differing at offset 0x1D0.
    • "generic/nand.uImage.aes": Starts differing at offset 0x0.
    • "generic/root.nand.cpio.gz_pad.img.aes": Starts differing at offset 0x0.