FiiO X1 firmware: a hidden USB DAC mode and a debug shell

FiiO's manual for the first-generation X1 and the 2014 Headfonics review agree on one thing about its USB port: it is a disk. Plug the $99 music player into a computer and it mounts as storage. If you want a USB DAC, the player working as your computer's sound card, the review says to look elsewhere, because "FiiO stripped that right out".

I read spec sheets the way I read a boot ROM's promises: by what they leave out. A missing feature on a cheap model of a product line is often still in the code, just not on the box. I had the X1's firmware decrypted, and Part 7 had matched it byte for byte against the copy on the chip, so it was time to check.

5,134 functions and no names#

The firmware is sys.bin: 4.1MB of µC/OS-II, a small real-time kernel, running on the X1's Ingenic JZ4760B MIPS chip. Load the decrypted image into Ghidra, the NSA's open-source disassembler and decompiler, as MIPS:LE:32:default at base 0x80001000 and run auto-analysis.

5,134 functions. A LiteGUI framework with Android-style activities, an embedded SQLite database, libpng, mpg123, zlib. A full application stack, built in 2016 for a $99 player.

Not one function has a symbol name. This is a stripped release binary: every function is FUN_80XXXXXX, every variable DAT_80XXXXXX. A 5,134-node call graph of anonymous nodes.

Where do you start? Strings.

"FiiO USB Audio Class 2.0 DAC"#

Ghidra's string search on a 4.1MB MIPS binary returns thousands of hits: debug format strings, file paths, error messages. One stands out:

FiiO USB Audio Class 2.0 DAC

A USB descriptor string, the name a host operating system shows when you plug in an audio device. The X1's official USB mode is mass storage, and nothing else.

The string has cross-references. Following them upstream leads to a function at 0x800197f0 that returns different descriptor strings depending on the mode. In DAC mode the manufacturer is "SmartAction" and the product is "FiiO USB Audio Class 2.0 DAC". In storage mode it is just "X1 USB-DISK".

Higher up, the init function at 0x80001df0 opens a one-byte file on the internal filesystem and picks the USB mode from it. Simplified from the decompile, with my names for its functions:

fd = file_open("z:\\system_attribute\\usbmode", "rb");
if (fd != 0) {
    read_value(buf, 1, 1, fd);
    if (strcmp(buf, "1") == 0) {
        usb_set_mode("usb_audio20");
    } else {
        usb_set_mode("usb_mass_storage");
    }
}

The same switch, numbered the other way round, sits in system_attribute/default.ini, the settings file that ships inside the firmware update image:

<usb_mode_type>1</usb_mode_type>  //0-USB DAC, 1-STORAGE

The comment is right there: 0 is USB DAC, 1 is storage, and the default is 1. The USB mode selector at 0x80033c60 uses the comment's numbering, 0 for usb_audio20 and 1 for usb_mass_storage, while the usbmode file wants a 1 for the DAC. How the file and the setting meet on a real boot, I didn't trace.

So the first-generation X1 carries a USB Audio Class 2.0 DAC path, with descriptor strings, driver selection and audio routing, and its shipped settings say storage. The PCM5142 DAC chip takes audio over I2S, and the JZ4760's USB controller would have to speak the Audio Class protocol in firmware, which is exactly the code sitting here. Whether it works on this hardware is a question for the next part. That it exists is not.

The X1 2nd generation, two years later and on a completely different Linux platform, also shipped without a DAC function; Headfonics' review says so in as many words. For PC DAC use, the same review sends you up a model, to the X3 2nd gen. Segmentation by product line. On this X1, it is one config value.

The configuration file#

That default.ini deserves a closer look. UTF-16LE XML, 55 settings, the device's whole personality in one file.

The expected: language (24 options in the comment), screen brightness (0-9), a sleep timer, volume (default 33 of 100). The less expected:

<seamless_playback_switch>0</seamless_playback_switch>  // 0-off, 1-on

Gapless playback, off by default. The switch exists, and it ships at 0.

A 10-band graphic EQ, 31Hz to 16kHz. The bands map onto the PCM5142's built-in miniDSP, seven biquad filters programmed over a three-wire serial bus the firmware bit-bangs on GPIO pins: it writes a 21-byte coefficient block per filter, taken from a lookup table at 0x803debe8, behind a 504-byte per-sample-rate preamble at 0x803de9f0, that it indexes in 147-byte steps (7 filters × 21 bytes) per gain level.

Three audio outputs in the config: headset, lineout, auto-detect. In the code, a third output target: SPDIF, digital out, on a player whose review notes that FiiO dropped the coaxial output along with DAC mode. The debug strings around it name one of its functions x5_audio_hw_suspend.

Then the Bluetooth entries:

<bluetooth_speaker_switch>0</bluetooth_speaker_switch>
<bluetooth_phone_switch>0</bluetooth_phone_switch>

There is no Bluetooth on the first-generation X1: no radio, and no driver code behind these two settings. Template leftovers. The config file does not know which model it is running on.

The keys in the car#

The firmware's authors left a debug console in the production build.

The prompt is (SAG). Nothing in the firmware spells it out; my guess is SmartAction Group, after the "SmartAction" that the USB descriptors give as the manufacturer. There are two ways in. The first is to crash the CPU: the exception handler at 0x80009800 dumps the memory around the crash address, the cause register with a decoded exception type, and all 32 general-purpose registers, renders that to the LCD if the display is up, and drops into the shell.

The second is an empty file called sa_debug.sa on the SD card, plus SELECT, PREV and NEXT pressed together:

if (file_exists("a:\\sa_debug.sa") || file_exists("b:\\sa_debug.sa")) {
    printf("lcd except debug open.\n");
    set_debug_level(1);
    debug_flag = 1;
}

The button check in the scan loop at FUN_80025030:

if ((buttons & 0x19) == 0x19 && debug_flag_check()) {
    debug_shell();
}

0x19 is bits 0, 3 and 4: SELECT, PREV, NEXT, all three held while the flag file sits on the card.

The shell has dir, copy, del, dump (hex dump any file), list (dump raw memory), xasm (disassemble memory), screen (screenshot to file) and format (format the disk). A reverse engineering kit, built into this firmware, one empty file and one chord away.

I spent five parts getting code onto this device the hard way, and it had a disassembler in it the whole time. In fairness to me, the shell needs the firmware booted, and none of those five parts ever had it booted.

Eight buttons, eight pins#

Cross-referencing the button scan function at FUN_80024e00 with the GPIO pin table at 0x8032BC54 maps the physical buttons to JZ4760 pins: SELECT on PD27, POWER on PE8, PREV on PC29, NEXT on PC31, VOL UP on PD17, VOL DOWN on PE9, MENU on PA30. All active-low.

The eighth, SHORTCUTS on PD11, is the odd one out. The main firmware never reads it. Only the SPL, the second-stage bootloader, does: hold it at power-on to enter firmware update mode. A hardware button with no runtime job, reserved for the boot chain.

The people behind the code#

The credits screen, activity lg_activity_staff, lists five developers under "DEVELOPMENT TEAM": Fanoble Meng, Errorcode Wong, Ringsd Deng, Kappen Van and Goff Tang, with their email addresses, which I'm leaving in the firmware where I found them. Next to them: "HibyMusic", market@hiby.cd, and a build stamp of June 7, 2016, 10:39:02. A USB mass-storage recovery descriptor elsewhere names the vendor as "DongGuanSmartAction Tech".

HiBy builds firmware for more than one brand, and by the credits, the X1's code was written there. That explains the rest. The Bluetooth settings, the DAC mode and the SPDIF routines all belong to one shared platform, and the X1 got that platform configured down to its price point.

The first-generation X1 was never the simple player its spec sheet described. It was a full HiBy audio platform, with a DAC mode, a debug shell, gapless playback, DSD-to-PCM conversion and an SPDIF path, configured down to the feature set its price allowed.

The spec sheet told the truth, as far as it went. The code was always there. The config just said no, and I have a patcher to write.

Filed under: reverse-engineering, mips, embedded, fiio, hardware

Sources#