FiiO X1 bare-metal audio: four GPIO offsets, all wrong

The build was clean: 18,672 bytes, nothing worse than unused-function warnings. A clean build is the most reassuring thing a compiler can say and the least informative, and this one was vouching for an audio driver that, as written, could not have made a sound.

The driver belongs to a bootloader for the FiiO X1, a $99 music player on an Ingenic JZ4760B MIPS chip. Part 13 wrote that bootloader: it runs from the SD card as factory.x1, a file the stock loader executes without checking, and it can load an image of Rockbox, the open-source player firmware. The next step toward Rockbox is sound, and the smallest honest test of sound is a sine wave from bare-metal code: no codecs, no DMA, no mixer. As far as I knew then, that meant three things the stock loader, which doesn't play music, has no reason to set up: an I2C controller to configure the TI PCM5142 DAC, the AIC (the chip's audio interface) to send it I2S samples, and the GPIO pins both of them need.

The audio chain on paper#

I2C for control, I2S for data. Both need their pins switched to the right alternate function, both need their clocks ungated, and the DAC needs an initialization sequence before it accepts samples. The new code came to 570 lines in seven files.

I2C0: four registers and a guessed address#

The JZ4760B's I2C0 controller sits at 0xB0050000, and Ingenic's headers put it on pins PE12 (SDA) and PE13 (SCK). It has four registers that matter here:

RegisterOffsetWidthPurpose
I2C_DR0x008-bitdata in/out
I2C_CR0x048-bitcontrol: enable, START, STOP, ACK
I2C_SR0x088-bitstatus: TEND, DRF, ACKF, BUSY
I2C_GR0x0C16-bitclock divisor

The divisor rests on an assumption written into the code's comment: PCLK is about 72 MHz, a quarter of a 288 MHz PLL, so 100 kHz needs GR = 72,000,000 / (16 x 100,000) - 1 = 44. Hold on to that 288.

The PCM5142's I2C address depends on how its ADR pins are strapped, so it could be 0x4C to 0x4F, and I have no schematic. The driver tries all four and keeps whichever answers:

static int i2c_scan_pcm5142(void)
{
    u8 addr;
    for (addr = 0x4C; addr <= 0x4F; addr++) {
        I2C_CR &= ~(I2C_CR_STA | I2C_CR_STO);
        I2C_DR = (addr << 1) | 0;
        I2C_CR |= I2C_CR_STA;
        if (i2c_wait_tend() < 0) {
            I2C_CR |= I2C_CR_STO;
            udelay(50);
            I2C_CR &= ~(I2C_CR_STA | I2C_CR_STO);
            continue;
        }
        I2C_CR &= ~I2C_CR_STA;
        if (!(I2C_SR & I2C_SR_ACKF)) {
            I2C_CR |= I2C_CR_STO;
            udelay(50);
            I2C_CR &= ~I2C_CR_STO;
            uart_puts("I2C: found DAC at 0x");
            uart_puthex(addr);
            uart_puts("\r\n");

If nobody acknowledges, the screen says DAC init failed! and I2C: no PCM5142 found, and MENU goes back.

AIC: I2S out of five pins#

The AIC generates the I2S signals: bit clock, frame sync, serial data out, and a master clock for the DAC. The pin assignment comes from Ingenic's own __gpio_as_aic() macro, all on port E:

PinSignalPurpose
PE22SDATIdata in (unused, configured anyway)
PE23SDATOsamples to the DAC
PE25SYNCleft/right frame sync
PE26BCLKbit clock
PE28MCLKmaster clock to the DAC

The clock chain, as the code computes it:

288 MHz doesn't divide evenly into any audio rate, so on paper the frame clock comes out about 2% fast, and the code relies on the PCM5142's internal PLL to cope with whatever MCLK arrives. The paper has a problem I already knew about: Part 5 found the IPL programming PLL0 for 384 MHz. If nothing later changes it, every clock in that figure runs a third fast, the I2C bus included.

The AIC's transmit FIFO is fed in polled mode, no DMA, no interrupts:

for (i = 0; i < count; i++) {
    /* Wait until TX FIFO has space (TFL < 32) */
    timeout = 100000;
    while (((AIC_SR & AIC_SR_TFL_MASK) >> AIC_SR_TFL_BIT) >= 30) {
        if (--timeout == 0) return -1;
    }
    AIC_DR = buf[i];
}

Spin on the fill level, push a sample. For a sine wave, enough.

PCM5142: the register sequence#

The DAC bring-up is a short run of register writes, each checked:

/* Reset */
if (dac_write(PCM_REG_RESET, 0x11) < 0) goto fail;
mdelay(100);
if (dac_write(PCM_REG_RESET, 0x00) < 0) goto fail;
mdelay(100);

/* Page 0 again after reset */
if (dac_write(PCM_REG_PAGE, 0x00) < 0) goto fail;

/* Standby for configuration */
if (dac_write(PCM_REG_POWER, 0x10) < 0) goto fail;
mdelay(1);

then the PLL reference and DAC clock source (0x10 each), the I2S format (0x02, 24-bit), both volume registers at 0x30 (0 dB), unmute, and out of standby. In the first version none of those writes was checked: one NACK and the code would have gone on pushing samples at a DAC still in reset.

The sine table and the phase step#

The tone comes from a 256-entry table at half amplitude, generated by:

amp = int(0.5 * (2**23 - 1))
entries = []
for i in range(256):
    val = int(amp * math.sin(2 * math.pi * i / 256))

The peak is 4,194,303. Half the 24-bit range leaves room for the loudest volume setting, +4 dB.

The table index advances by a 16.16 fixed-point phase step:

phase_step = (test_freqs[freq_idx] * 65536u) / (SAMPLE_RATE / SINE_TABLE_SIZE);

44100 / 256 truncates to 172, so for 440 Hz the step is 440 x 65536 / 172 = 167,650, about 2.56 table entries per sample and 100 samples per cycle. The tone comes out near 440.7 Hz. Close enough for a test.

The playback loop fills 64 stereo pairs, pushes them to the FIFO, then checks buttons: six frequencies from 220 Hz to 4 kHz, six volume steps from -16 dB to +4 dB, SELECT to play or stop, MENU to leave.

What two code reviews found#

The device has no JTAG, no debugger, and recovering from a hang means holding the power button for fifteen seconds. On that hardware a register bug gets found by elimination: change something, rebuild, copy factory.x1 to the card, boot, watch, repeat. So before any of it went near the card, the code got two reviews, the second the next morning.

Round 1 found three critical bugs and six important ones. Three of the important ones asked for checks rather than changes: the DAC's PLL reference depends on board wiring, the sample packing was fragile but correct, and the 2% fast MCLK is the clock chain above. The fixes:

  • Phase step overflow. The first version computed (SINE_TABLE_SIZE * test_freqs[freq_idx] * 65536) / SAMPLE_RATE. 256 x 65536 is 2^24, so any frequency of 256 Hz or more overflows 32 bits: five of the six test tones. Divide first, as above.
  • No BCLK divider. AIC_I2SDIV was never written, so the bit clock would have come out of whatever the register held.
  • No AIC pins. The interface was configured and its pins weren't, so the I2S signals would never have left the chip.
  • Register collision. PCM_REG_DSP_CLK and PCM_REG_ERROR were both defined as register 37. DSP_CLK is 27.
  • Silent I2C. No dac_write result was checked, the fire-and-forget described above.
  • Two AIC error-flag bits in the wrong positions, and, from the review's suggestions, a DAC reset delay of 10 ms, now 100.

I found one more myself, checking the build: the sine table I'd pasted in earlier peaked at entry 34 instead of 64 and wandered below zero where it shouldn't. The table above is its replacement.

Round 2 found five critical and five important, and the worst ones were in the register header, jz4760b.h, which everything includes. Four GPIO offsets were wrong:

#define GPIO_PXFUNS(n)  REG32(GPIO_BASE + (n)*0x100 + 0x44)
#define GPIO_PXSELC(n)  REG32(GPIO_BASE + (n)*0x100 + 0x54)
#define GPIO_PXSELS(n)  REG32(GPIO_BASE + (n)*0x100 + 0x58)
#define GPIO_PXTRGC(n)  REG32(GPIO_BASE + (n)*0x100 + 0x6C)
#define GPIO_PXPES(n)   REG32(GPIO_BASE + (n)*0x100 + 0x74)

PXSELC clears a pin's select bit and PXSELS sets it, and the real offsets are the other way round: 0x58 and 0x54. Every "clear" set the bit, every "set" cleared it. 0x74 isn't a pull register at all; it's PXTRGS, trigger set, so every pull-resistor call set a pin's trigger bit instead. And PXTRGC belongs at 0x78, not 0x6C. The fix came from the authoritative jz4760.h in Ingenic's sources:

MacroBeforeAfterWhat the wrong value did
GPIO_PXSELC0x540x58set SEL instead of clearing it
GPIO_PXSELS0x580x54cleared SEL instead of setting it
GPIO_PXPES0x740x34set TRG instead of touching pulls
GPIO_PXTRGC0x6C0x78pointed at an undefined offset; nothing called it yet

The I2C setup writes FUNS, SELC and PES for PE12 and PE13 to get alternate function 0: FUN=1, SEL=0, TRG=0. With the old header it would have produced FUN=1, SEL=1, TRG=1, a different function on the same pins. The START conditions would have gone out on pins wired to some other peripheral, and the first symptom would have been "no DAC found", which points you at the DAC.

I'll give this round's award, the seventh in the series, to the old GPIO_PXPES line: Best Impersonation of a Pull Resistor. Ingenic's header calls the real one at 0x34 the pull-disable set register; this one was named and used as that, and it set trigger bits with total conviction.

The rest of round 2:

  • AIC pins, take two. The round 1 fix had configured PE22, PE23, PE25 and PE26 and missed PE28, the master clock. The new code is an exact copy of __gpio_as_aic():
GPIO_PXFUNS(4) = 0x16C00000;
GPIO_PXTRGC(4) = 0x02C00000;  /* PE22,23,25 TRG=0 */
GPIO_PXTRGS(4) = 0x14000000;  /* PE26,28 TRG=1 */
GPIO_PXSELC(4) = 0x14C00000;  /* PE22,23,26,28 SEL=0 */
GPIO_PXSELS(4) = 0x02000000;  /* PE25 SEL=1 */
GPIO_PXPES(4)  = 0x16C00000;  /* pull disable */
  • BCLK divider. The round 1 fix wrote (256 / 64) - 1, 3; the register takes the divisor directly, 4.
  • I2S format. The DAC was set to 0x00, 16-bit, while the AIC sent 24-bit samples. Now 0x02.
  • Sample packing. Samples were shifted left 8 bits for a FIFO that, with 24-bit output selected, most likely takes them right-justified; the review said "likely" too, and only hardware can settle it.
  • Underrun flag. AIC_SR_TUR was bit 1; it's bit 5.
  • No way back. Leaving the audio test hung; now it returns to the menu.

One important finding stayed open: redrawing the screen on a button press drains the FIFO, so every press while a tone plays should click. For a test tool, that's fine.

Why the header looked fine#

The bootloader from Part 13 used the same jz4760b.h, with the same wrong offsets, and nothing in it noticed. It never calls PXSELC, PXSELS or PXPES: the SD card and LCD pins it uses were already configured by the stock loader before factory.x1 started. And that bootloader has never run on the device either, so even a bug in what it does use couldn't have shown up yet.

Audio is the first code that has to configure pins from scratch. The header's numbers only started to matter when something read them.

The tally#

18,864 bytes, 21 source files, about 2,230 lines, up from 12,512 bytes and 1,582 lines in Part 13. A third menu entry: Rockbox, Original FW, Audio Test. I2C scan, DAC init, AIC clocks, sine table, polled FIFO, button control. Two review rounds, 16 fixes from them, eight critical, plus the sine table I caught myself.

None of it has run on the hardware.

NoteCorrection, 2026-05-31

Five days after this part, reading the stock firmware's audio driver showed that the PCM5142 hangs off no I2C controller: the firmware drives it over a bit-banged three-wire bus on GPIO pins: PB28 clock, PB21 data, PC15 latch. There is no I2C address to scan. The offsets above were wrong and are fixed, but the I2C half of this driver, PE12, PE13 and the 0x4C-0x4F scan, was aimed at a bus the DAC isn't on, and the audio-chain figure shows that guess. That earns award number eight, which I'm keeping for myself: Most Thoroughly Reviewed Driver for a Bus That Isn't There.

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

Sources#