FiiO X1 PCM5142 DSP blocks: the flat head I believed
Contents
For the length of one hex dump, three blocks of DAC coefficients in the FiiO X1's firmware were solved. I had read the first 96 bytes of each, seen nothing but unity filters, which pass the signal through unchanged, and written down that all three were a flat, do-nothing initialization. Then the decoder read the rest.
The X1 is a pocket music player from 2014 whose sound comes out of a TI PCM5142, a DAC with a small programmable DSP inside it. The firmware, HiBy's, on the µC/OS-II kernel, talks to the DAC over a three-wire serial line it drives by toggling GPIO pins; Part 9 found that line and the equalizer table behind it, and Part 15 decoded that table. My notes on the audio code still listed three other blocks as undecoded: 2,708 bytes each, sitting just below the equalizer table in memory, streamed to the DAC every time the firmware opens an audio stream. Nobody puts 8 KB of nothing in a firmware image and then sends it to a chip on every stream, which should have been my first clue.
The first 96 bytes: unity filters#
A block is a stream of byte pairs, a DAC register and the value to write into it. Register 0x00 selects a page of the DAC's register space; the rest are data. The head of the first block selects page 0x2c and then fills registers 0x08 onward in order. At registers 0x08, 0x1c and 0x30 the data starts 7f ff ff, and everything after it is zero.
The PCM5142 stores a biquad, the standard two-pole filter, as five coefficients, b0, b1, b2, a1, a2, in twenty consecutive registers, and these blocks hold each one as four bytes in signed 1.31 fixed point, raw / 2^31. The first section reads:
b0 = 0x7FFFFF00 → +1.000000 (1.31 fixed-point)
b1 = 0x00000000 → 0.000000
b2 = 0x00000000 → 0.000000
a1 = 0x00000000 → 0.000000
a2 = 0x00000000 → 0.000000
b0 = 1 and nothing else: the output equals the input. The next sections in those 96 bytes look the same, and the whole of page 0x2c, six sections in 242 bytes, is unity or empty. The head of the block is telling the truth about itself, and I extended that truth to 8 KB of data I hadn't read. Mock award number nine goes to me, for the Shortest-Lived Finding in the Series: my note that the three blocks were identical and flat, which lasted from one look at a hex dump to the first run of a decoder, and was wrong about the blocks being identical as well as flat.
What the decoder found: 66 slots, 20 of them unity#
A 2,708-byte block is 1,354 pairs. Its page selects visit fourteen pages, in three groups:
0x2c,0x2d,0x2e: the first coefficient bank0x3e,0x3f,0x40: a second bank, byte-identical to the first where they overlap; the notes read it as a double buffer0x98to0x9f: eight pages, the largest group
A full page holds six biquad sections, at registers 0x08, 0x1c, 0x30, 0x44, 0x58 and 0x6c, which gives 66 slots per block. Read that way, the three blocks come out like this:
| Block | Address | Sample rates | Unity or empty | Other |
|---|---|---|---|---|
| blk1 | 0x803dca1c | below 64,001 Hz (8 to 48 kHz) | 20 | 46 |
| blk2 | 0x803dd4b0 | below 96,001 Hz (88.2, 96 kHz) | 22 | 44 |
| blk3 | 0x803ddf44 | up to 192,000 Hz (176.4, 192 kHz) | 24 | 42 |
The three blocks are the same length and different bytes, and the count of non-unity slots falls as the sample rate rises.
The first bank is where the flat reading came from, and even it isn't flat. Page 0x2d has two slots that are not unity:
pg 0x2d slot 0x58: b0=+0.898438, b1=-0.000000, b2=-1.000000, a1=+0.500000, a2=+1.000000
pg 0x2d slot 0x6c: b0=+0.000000, b1=+0.000000, b2=+0.000000, a1=-0.500000, a2=+0.148438
My notes call the first one a high-pass or DC-blocker stage. With a2 = +1.0 in the denominator I would not swear to that, and the second one has an all-zero numerator, which as a filter would output silence. Whatever these two are, a passthrough is not on the list.
The big bank: 46 is a count, not a diagnosis#
The eight pages from 0x98 hold most of the non-unity slots, and their numbers have a habit:
pg 0x98 slot 0x1c: b0=+0.002931, b1=+0.013184, b2=+0.001954, a1=+0.003418, a2=+0.013185
pg 0x98 slot 0x30: b0=+0.003420, b1=+0.002933, b2=+0.013186, a1=+0.009280, a2=+0.013187
pg 0x98 slot 0x44: b0=+0.003422, b1=+0.002935, b2=+0.013189, a1=+0.009282, a2=+0.013190
The same handful of values, near 0.0029, 0.0034, 0.0093 and 0.0132, turn up slot after slot, now and then one position along, creeping upward in the sixth decimal. That is not what a chain of 46 tuned filters looks like. It looks like data with its own structure seen through a grid of five 4-byte coefficients, or like something that isn't biquad coefficients at all. So "46 active filters" in my notes is a count of slots that aren't unity. What those slots are is a separate question, and I don't have its answer.
pcm5142_load_dsp_coeffs: one block per sample rate#
The code that sends the blocks is pcm5142_load_dsp_coeffs at 0x8002093c. It sets a busy flag, streams a 12-byte preamble from 0x803dca00, then the block for the current sample rate, then a 16-byte suffix from 0x803dca0c, and clears the flag. Above 192 kHz there is no rate block to send. The lengths come from a header of five little-endian 32-bit words at 0x803de9d8 (file offset 0x3dd9d8): 12, 16, and 2,708 three times.
Each pair goes through pcm5142_dsp_write_pair at 0x80020908, which packs it as [reg << 1, val] and hands it to the same bit-banged three-wire writer as every other register the firmware sets on this DAC, clocked on PB28 with data on PB21 and a latch on PC15. The preamble's six writes put the DAC's DSP into a state for loading coefficients; the suffix's eight, which include register 0x2b set to 0x1f, read as the commit that swaps the new bank in. Those two readings come from the register numbers, not from TI documentation of these pages, which I don't have.
Two PCM5142 tables, two formats#
These blocks and Part 15's equalizer table sit next to each other in the firmware and reach the DAC over the same wire. Most of the rest differs:
| EQ table (Part 15) | DSP blocks (this post) | |
|---|---|---|
| Address | 0x803DE9F0 (preamble), main table 0x803DEBE8 | 0x803dca1c, 0x803dd4b0, 0x803ddf44 |
| Size | 504 + 8 × 1,911 bytes | 3 × 2,708 bytes, plus 28 bytes of preamble and suffix |
| Coefficient format | 24-bit Q23, 3 bytes big-endian | 32-bit 1.31, 4 bytes big-endian |
| Keyed by | sample rate and gain level | sample-rate group |
| Sent by | pcm5142_set_eq | pcm5142_load_dsp_coeffs, each time a stream opens |
The equalizer table is the layer people touch: gain levels per band, chosen on the EQ screen. The DSP blocks sit under it, keyed only by sample rate, and nothing the user sets changes them. A third rate-keyed table turned up in the same sweep of the firmware: the AIC (audio interface controller) clock dividers at 0x8032ad40, 117 records of 24 bytes. Bringing up audio from scratch means all three.
Replay, don't decode#
For an alternative firmware, Rockbox or anything else that drives this DAC, the practical answer doesn't depend on what the slots mean. The blocks are self-contained register streams: after opening the DAC, send the preamble, the block for the sample rate and the suffix, pair by pair, through the same three-wire write. What the DAC does without them, with whatever defaults it powers up with, I haven't heard; nothing in this post ran on hardware. It is static work on the decrypted firmware, a decoder script (pcm5142_dsp_coeffs.py) and its full dump.
The DSP blocks are a photograph of a program I can copy and can't yet read. The head said flat, and the head was right about itself. I'm the one who believed it about 8 KB I hadn't looked at.
Filed under: reverse-engineering, mips, embedded, fiio, hardware, audio, dsp
Read next#
- Decoding the EQ coefficient table: the PCM5142's other table, and a layout I also got wrong
- The USB DAC and debug shell in the firmware: where the DAC's three-wire bus first turned up
- The FTL behind the broken carve: the other half of the same static session
Sources#
- TI PCM5142 Datasheet — miniDSP architecture, coefficient RAM, page addressing
- TI PCM5142 Biquad EQ Calculation (E2E Forum) — biquad coefficient format context
- Rockbox packtools — XOR decryption, firmware unpacking