Xbox controller firmware update protocol (UFU over GIP)
Microsoft’s controllers are updated with a protocol that Windows calls UFU,
carried in GIP messages over the controller’s interrupt endpoints (interface 0,
OUT 0x02, IN 0x82). It was reverse engineered from the Windows driver and
Windows.Gaming.Input.dll, then verified against the QEMU emulator and a
physical Xbox Series X|S controller. All integers are little-endian.
This is the protocol xboxctl implements; the source is in
src/gip/.
Contents
GIP framing
Every packet starts with command, flags, sequence, length, where the length
is a base-128 varint. Relevant flags:
| flag | meaning |
|---|---|
0x10 |
acknowledgement requested |
0x20 |
system message (all UFU traffic) |
0x40 |
first fragment |
0x80 |
fragment |
Sequence numbers run from 1 to 255; 0 is skipped. A Series X|S controller (firmware 5.23) ignores a rumble packet unless its number is ahead of the last one it received, compared with wraparound like TCP sequence numbers, and it keeps that number after the host program exits; see Rumble below.
- Acknowledgement (command
0x01):01 20 <seq> 09 | 00 <command> 20 <received:u16> 00 00 <remaining:u16>. The controller requests one for each of its UFU replies (0c 30 …), and the host has to send it. - Fragmentation: messages that don’t fit one 64-byte packet are split into
58-byte fragments with 6-byte headers. Host-to-device headers must have an
even length, so one varint is padded to two bytes. The first fragment carries
the total length, later ones their offset. Acknowledgements are requested on
the first and last fragment and periodically in between (
xboxctl: every fifth); the receiver reports the contiguous bytes received, and the sender resends from there. A zero-length fragment at offset = total ends the message. - Set Device State (command
0x05, one-byte payload):00start,07reset. - Status (command
0x03): a first byte with bits 6–7 clear means powered off. -
Rumble (command
0x09, flags00, nine-byte payload):00 <motors> <LT> <RT> <left> <right> <duration> <delay> <repeat>. Motor bits0x0fenable all four motors, strengths are 0 to 100, and durationff, delay00, repeatffkeep running until the next rumble packet, so stop with all strengths 0.xboxctl rumblesends it, and the Linux xpad and xone drivers send the same packet (xone uses repeateb). The controller does not acknowledge it.Rumble is dropped silently unless its sequence number is ahead of the last one the controller saw, within a window of at least 75 numbers. Observed on a Series X|S, firmware 5.23: after a run that ended at about 125, restarting at 100, 60 or 62 did nothing, while 120, 200 and then 10 (across the wrap) worked. A replug forgets the number. A program that restarts at a fixed number therefore works once and then stops working until the next replug. The Linux xpad and xone drivers avoid it by keeping one counter for the whole connection. When a program cannot know the last number, it can first send stop packets whose numbers are 32 apart around the whole cycle, as
xboxctl rumbledoes.
UFU messages
UFU requests and replies are the payload of GIP command 0x0c with the system
flag. Replies echo the opcode, followed by a status byte.
| op | request | reply |
|---|---|---|
00 GetInfo |
00 |
60 bytes: 00 st protocol flags vid:u16 pid:u16 max_write:u32 … |
01 GetVersion |
01 component:u32 |
01 st component:u32 version:u16×4, optionally magic 0x1A253A45, partition_address:u32, partition_size:u32 |
02 BeginUpdate |
02 component:u32 address:u32 size:u32 version:u16×4 |
02 st |
03 GetBlockHash |
03 address:u32 length:u32 |
03 st sha1[20] of the flash in that range |
04 WriteBlock |
04 00 00 00 address:u32 data (fragmented) |
04 st |
05 CompleteUpdate |
05 |
05 st |
Status codes: 0 ok, 1 invalid argument, 2 unknown component, 3 must reset, 4 component in use, 5 invalid range, 6 write failure, 7 write protected, 8 update in progress, 9 update not in progress, 10–12 EEPROM errors, 13 protocol error, 14 incompatible version.
Protocol 1 is the original Xbox One controller’s legacy variant and is not supported. The Series X|S controller reports protocol 2 and a 4 KiB write size.
Update sequence
- Start the controller (Set Device State
00). - GetInfo: the VID/PID must match the package. Blocks are
min(max_write, 0x1000)rounded down to 4 bytes. - GetVersion for every component in the package. Components whose flags have bit 0 set (critical, e.g. the bootloader) are never written. A component larger than the reported partition does not fit this controller.
- Order: if component 3 fits its partition (
size < partition_size), update in the order 4, 7, 3, 2, 1, otherwise 2, 1, 4, 7, 3; ids not in the package are skipped. The partition size comes from GetVersion if reported. A Series X|S controller reports a 0x7e000 partition for component 3, so 5.23 packages are written in the order 2, 4, 3. - For each component whose version differs: BeginUpdate, then for every block GetBlockHash and, if the SHA-1 differs, WriteBlock, then CompleteUpdate.
- Repeat the version check (up to 10 passes) until every component matches, or until a pass writes nothing.
- Reset (Set Device State
07). The controller restarts into the new firmware.
The Windows updater waits 4 s for each reply (6 s for WriteBlock) and sends each request up to 5 times.
Restarts
When a reply has status 3 (must reset), the host resets the controller and continues once it is back. The Series X|S controller asks for this after every CompleteUpdate and once before writing component 4, so a full update restarts it five times, including the final reset.
A restart looks like this on the wire:
- The host sends Set Device State
07. - The controller replies with a powered-off status (
03 20 ss 04 00 …). - 1–3.6 s later it drops off USB, then re-enumerates at a new address.
- It sends one Hello, which the host must answer promptly with Set Device
State
00and a non-zero sequence number. Windows answers within 15–31 ms. A controller whose Hello is not answered stops responding until it is unplugged. The Windows updater gives the controller 30 s to come back (60 s after component 7, which only the Adaptive Controller has). - The update continues with GetVersion; a component completed before the restart now reports its new version.
Stick calibration
Command 0x1e with flags 10 (the sender asks for an ACK) starts and reads the
stick calibration. It is what the Xbox Accessories app uses (StartCalibration
and GetCalibration); the layout was observed on a Series X|S controller,
firmware 5.23.6.0. The masks select the stick: bit 0 left, bit 1 right.
- Read: payload
0f <mask>. The reply is0f 00 <status>followed by one block per selected stick:<mask> <h_min> <h_max> <v_min> <v_max>, four u16. The values are raw readings of about 12 bits (centre near0x800). Zero limits mean that no calibration is stored, and the controller then uses built-in defaults. - Start: payload
10 <mask>, reply10 00 <mask>. The controller erases the stored limits of those sticks right away. - Status of a read:
00idle, bit 0 or bit 1 while that stick is being calibrated,10for about half a second while the result is saved. - Finishing: there is no commit message. The controller finishes by itself
once the stick has been turned through its range; it did so after about 6
clockwise and 5 counter-clockwise turns (the Xbox Accessories app asks for 6
each, then pushes to up, right, down and left, which the controller did not
need). The status goes to
10and then00, with the learned limits stored in non-volatile memory. They were still there after a power cycle. - Interruption: the erased limits stay erased if the calibration is not
finished. A Start while another one is still pending did not finish; a reset
(Set Device State
07) clears the pending state. - The controller also sends a one-byte message with command
0x77while it calibrates;xboxctlonly acknowledges it. - Older firmware does not answer a read at all: a Series X|S on 5.13.3146.0 ignored it, with no ACK either. The Xbox Accessories app is reported to offer calibration only from 5.19.3221.0; versions in between were not tried. The stored limits were unchanged after updating down to 5.13.3146.0 and back to 5.23.6.0, so they are kept outside the firmware image.
The controller sends input reports (command 0x20: buttons u16, two triggers
u16, then left X, left Y, right X, right Y as i16, Y up) only when something
changes, so a stick at rest is silent. xboxctl calibrate follows the rotations
and the final check with them.
Trigger calibration
The same command 0x1e calibrates the triggers, with mask bit 2 for the left
trigger and bit 3 for the right one (checked on a Series X|S controller,
firmware 5.23.6.0). Sub-command 0f reads and 10 starts, as for the sticks, and
the controller finishes by itself after about 12 full presses of the trigger.
- Read reply:
0f 00 <status>, then one block per trigger of 6 bytes: the mask, a flag byte (02on both triggers), and the little-endian u16 minimum and maximum of the raw reading, for example04 02 6d 04 df 09, a range of 1133 to 2527. A stick block is 9 bytes, so a reply is read block by block. - Input report: the triggers are the u16 values at payload bytes 2 to 5, from 0 to 1023, and are only sent when they change.
Series X|S controller (045e:0b12, MERL) layout
| id | flash address | partition | contents |
|---|---|---|---|
| 1 | 0x10000000 |
0x5000 |
bootloader; never updated (5.9.2709.0 on the tested controller) |
| 2 | 0x10005000 |
0x22000 |
encrypted or compressed image |
| 3 | 0x10027000 |
0x7e000 |
application (Cortex-M); its version is the release version |
| 4 | 0x10027000 |
0x7e000 |
second application image, purpose unknown; own version series (e.g. 5.23.330.0) |
The signed components (1, 3, 4) end in a 0x180-byte block, probably an RSA-3072 signature that the controller checks; the host performs no signature checks.
rdhm packages
Firmware files (MERL_045E_0B12_v_5_23_6_0_h_<sha256>.bin) are rdhm
containers. The <sha256> in the name is the base64url SHA-256 of the file.
| offset | field |
|---|---|
0x00 |
magic rdhm |
0x04 |
header version (1) |
0x08 |
VID:u16, PID:u16 |
0x0c |
package version u16×4 |
0x14 |
minimum version u16×4 |
0x1c |
number of components (≤ 10) |
0x40 |
component entries, 0x40 bytes each |
Each entry holds the component id, flags (bit 0 critical), file offset, size, target address, an unused word, version u16×4 and minimum version u16×4. The file is exactly the header, the entries and the component data; the host sends only the component data, never the header.
Firmware feed
https://aka.ms/xboxaccessoriesfwcdnpcga redirects to the JSON index the Xbox
Accessories app uses: g feed name, v version, p download base URL, and
d devices. Each device has __n codename, id (VID_PID, sometimes with a
-cc- suffix), f firmware files (p file name) and req (minimum version).
A firmware file is downloaded from p + its file name.