Skip to content

n31: EIC, GPIO, PMIC keys, I2C, USB gadget - #1

Draft
andrew867 wants to merge 31 commits into
freemyipod:s5l87xxfrom
andrew867:n31/glass-peripherals
Draft

n31: EIC, GPIO, PMIC keys, I2C, USB gadget#1
andrew867 wants to merge 31 commits into
freemyipod:s5l87xxfrom
andrew867:n31/glass-peripherals

Conversation

@andrew867

@andrew867 andrew867 commented Aug 23, 2026

Copy link
Copy Markdown

N31 peripherals that run on a real nano 7G with single commit per device, on top of s5l87xx.

Home, Sleep, Play, Vol±, the panel, backlight, I2C, and Windows RNDIS all work on glass. Kernel 6.14.0 #90.

What we tested

  • DFU → 3-image (zImage:ramdisk:dtb) → tty0 # shell
  • Home / Sleep / Play from D1830 nIRQ (GPIO 86, level-low into the EIC)
  • Vol+ / Vol- from SoC GPIO 40/41. The driver polls DIN. gpio-keys-polled is off on purpose: that path issues GPIOCMD 0xFFFE and the pads go quiet
  • Sleep held ~2s → pm_power_off (D1830 reg 13 bit 0)
  • TinyDRM + MMIO backlight. CON/PHTIME are logged and left as U-Boot wrote them
  • Windows 10/11 inbox RNDIS, 1d6b:0106. SSH and telnet to 192.168.7.2

I2C completion on this SoC is IICCON bit 4 (IRQPEND), not the +0x20 INT word. STAT bit 0 is not NAK here; treating it as NAK broke PMIC@0x73.

What we left out

I2S / CS42 PCM is not in this series. We can program the analog path. The SoC serializer still does not drain the FIFO, so there is nothing useful to review yet.

Also out: PL080 tone, Nimbus, BCM2078, Tristar-as-working, LIS3 “fixes”, and dlg,apply-sec-rails. That last one writes the power-off latch at probe.

Other iPods

N46 and N20 still bind apple,s5l87xx-usb to dwc2_set_s5l8702_params (slave PIO, session-valid GINTMSK quirk). N31 uses apple,s5l8740-usb / apple,s5l8740-otgphy so it does not inherit the nano3 PHY ramp or that quirk.

Clock ungate is limited to samsung,s5l8740. We do not write CLKCON +0x00/+0x04 (SYS / DRAM) or +0x50 (fatal latch).

Commits

  1. irqchip: S5L8740 GPIO EIC
  2. gpio: banked S5L8740 + GPIOCMD
  3. i2c: s5l8702 IRQPEND
  4. gpio: d1830 nIRQ / keys / power-off
  5. phy: s5l87xx OTG sequence
  6. usb: N31 DWC2 params + Windows RNDIS
  7. clk: PWRCON ungate on N31
  8. video: backlight + TinyDRM pitch / handoff
  9. dts/defconfig: wire the above
  10. arm: early WDT disarm (CON=0, CNT=0)

Happy to split any of these into their own PRs if that is easier to review.

andrew867 and others added 30 commits August 23, 2026 20:58
N31 GPIO interrupts go through an EIC at 0x39700000, then into the
PL192 VIC EXT lines. Group is gpio >> 5. Chain only the parents
listed in DT; wiring every EXT0..6 hung boot.

GPIO 86 (PMIC nIRQ) is group 2 on VIC EXT3. Vol± stay on SoC GPIO
and do not need this chip.

Tested on iPod nano 7G.
RetailOS programs pads with a word at 0x3CF001E0:
  (bank << 16) | (pin << 8) | cmd
DIN is bank+0x04, DIR is bank+0x14. The 2-line bcm6345 hack on
0x3CF000A4 is not enough for Vol± or for EIC to_irq.

apple,skip-sec-pinmux leaves U-Boot/SEC leftovers alone and only
GPIOCMDs the IIS0 pair plus GPIO 86. Replaying the full SEC table
from Linux broke buttons and USB on glass.

gpio-keys-polled on GPIO 40/41 issues mode 0xFFFE and the pads
go quiet. This driver polls DIN itself.

Tested on iPod nano 7G: Vol+ / Vol- report KEY_VOLUMEUP/DOWN.
N31 IIC1 never sets the +0x20 INT word. Byte done is IICCON bit 4
(S3C IRQPEND). The old ISR read +0x20, returned IRQ_NONE, and left
SCL stretched.

Clear IRQPEND to resume the bus. Do not treat STAT bit 0 as NAK;
that aborted every PMIC@0x73 transfer with -EIO even when VIC 22
fired. On reads, the first IRQPEND is address-complete and DS still
holds the address byte. Skip that one, then take one IRQPEND per
RX byte.

PIO mode stays available but is not required. IIC1 at 0x3C900000
is the PMIC bus.

Tested on iPod nano 7G: D1830 @0x73 and LIS3 @0x18 complete.
Home / Sleep / Play are PMIC bits (regs 7 and 8), not SoC GPIO.
OSOS unmasks them and uses GPIO 86 as an active-low nIRQ into the
EIC. gpio-keys-polled on these lines hammers I2C and was already
disabled in the N31 DTS.

Register an input device and a threaded nIRQ. Keep a slow poll so
a missed EIC edge still shows up.

Do not write register 13 at probe. Bit 0 is the power-off latch
and it cuts Vbat. The old default rail sequence did that. Rail
bring-up stays behind dlg,apply-sec-rails, which N31 must not set.

pm_power_off writes reg 13 bit 0. Sleep held ~2s still uses that
path.

Tested on iPod nano 7G: Home, Sleep, Play, and Sleep-hold poweroff.
The 8702 analog-stage ramp (CTRL1/CTRL2 walk) is for nano 3G. On
nano 7G it drops the Lightning link while DWC2 still probes.
Windows then sees VID_0000&PID_0002.

N31 matches U-Boot s5l87xx-otg-phy: drop D+ (DCTL SFTDISCON),
clear PCGCCTL, then PWR/RSTCON/MODE/CLK. Compatible strings
apple,s5l87xx-otgphy and apple,s5l8740-otgphy select that path.
apple,s5l8702-otgphy keeps the old ramp for N46.

Tested on iPod nano 7G: gadget enumerates after DFU.
N46 and N20 keep apple,s5l87xx-usb → dwc2_set_s5l8702_params
(slave PIO, session_valid_gintmsk_quirk). N31 uses
apple,s5l8740-usb → dwc2_set_s5l87xx_params.

That setter follows RetailOS sub_1B543A: NP=32, first dedicated
IN capped at 512 words, buffer DMA, GAHBCFG INCR8. The 8702
quirk masks USBRST/EP0 until SessReqInt, which does not arrive
after the 87xx PHY reset, so GET_DESCRIPTOR dies as 0000:0002.

RNDIS already leaves cdc_filter=0 until SET. Apply the Windows
inbox bits: IAD class/subclass/protocol on the control iface
(usbccgp / rndiscmp.inf want EF/04/01 there), INIT reports
media connected, LINK_SPEED stays non-zero.

Tested on Windows 10/11 against 1d6b:0106 from our gadget:
Remote NDIS Compatible Device, SSH and telnet to 192.168.7.2.
N31 boots with WTF/U-Boot leftovers. clk_disable_unused then
writes SET_TO_DISABLE gates and peripherals drop. Clear the
known PWRCON banks and the CG16 enable bits in the divider
regs, and mark the published gates CLK_IS_CRITICAL |
CLK_IGNORE_UNUSED.

Never write CLKCON+0x00/+0x04 (SYS PLL / DRAM) or +0x50
(fatal latch). The ungate walk is limited to samsung,s5l8740
so N46 keeps the old probe.

Tested on iPod nano 7G: I2C, GPIO, LCDIF, and DWC2 stay
clocked after late init.
Backlight is a separate MMIO block at 0x3E000000. Level is
1..62 at +0x08. This driver does not touch LCDIF CON or
PHTIME.

TinyDRM was blitting fb->width * fb->height as a linear
buffer. fbcon pitch is wider than 240, so the panel tore.
Walk each row with pitches[0]/4. Log CON/PHTIME at probe
and leave the values U-Boot wrote.

Tested on iPod nano 7G: tty0 shell and backlight at 62.
Replace the 2-line bcm6345 GPIO hack with the banked
s5l8740 GPIO and the EIC. Home/Sleep/Play come from D1830
nIRQ on GPIO 86; do not poll those bits over I2C.
gpio-keys-polled stays disabled so it cannot GPIOCMD 0xFFFE
the Vol pads.

USB uses apple,s5l8740-usb / apple,s5l8740-otgphy so N31
does not inherit the nano3 PHY ramp or the 8702 DWC2 quirk.
syscon-reboot is disabled: 0x100000 arms the watchdog.

No I2S, PL080, CS42, or dlg,apply-sec-rails. Those are not
ready.

Tested on iPod nano 7G with apple_n31_defconfig.
WTF and U-Boot leave CON/CNT armed at 0x3C800000. A bigger
zImage loses the race and resets in the middle of decompress
or early device probe.

Write CON=0 then CNT=0, twice. Never CLKCON+0x50.

Tested on iPod nano 7G: kernel #90 reaches tty0.
Land the in-tree module set used on device (#90+): FMSS CS/META weave
scan and FTL helper, Nimbus touch, PL080 DMA, CS42L81/I2S audio stubs,
Tristar mux, plus DTS/defconfig wiring and RNDIS/gadget tweaks from the
lab tree. Keep FMSS/FTL as loadable modules (CONFIG_FMSS_S5L8740=m).
Peripheral does not keep chaining state between operations. IV needs to be set explicitly before CMD_START.
Fail probe if apple,cpu or CS42 node is missing instead of falling
back to string names. Codec DAI remains cs42l81-hifi (no COMP_DUMMY).
Match tools/linux-n31/Kconfig: SND_SOC_APPLE_NANO7 selects
SND_SOC_APPLE_CS42L81_SPI; CS42 depends on SPI && SND_SOC.
Sustained IIS0/PL080/ALSA playback at 44.1 kHz, CS42 5707D8 path, MikeyBus jack helper, and current FMSS/FTL sources. Analog HP still unproven on UCA222.
Quiet Tristar I2C -110 storm (poll_ms=0, NACK stops poll), DMA-meta
storage path with meta_dma_read default off until CS kick is safe,
PMIC/CS42 glass fixes, and nodrm DTS.
Replace retired fmss monolith with nand-s5l8740 FIL and multi-object
ftl (core/csmap/vecmap) including CXT→BTOC→L2V recover on CS META.
Sync MikeyBus, Tristar, Nimbus, and N31 DTS from live glass bring-up.
Storage bring-up milestone for the iPod nano 7 (S5L8740), plus the code
cleanup that should have accompanied the earlier syncs.

FTL / NAND
----------
Use the SFTL context block as the primary map source instead of replaying
every open superblock. On the glass this takes a cold recover from 498 s to
69 s while mapping more of the volume (642652 -> 811554 LBAs), and only 8 of
1803 superblocks then need replaying.

The key finding is that CXT VBAs are not in the address space
whimory_pack_vba() builds. The FTL counts one superblock as the same virtual
block across every (ce, cau) plane, so a superblock holds 2048 VBAs and the
plane index sits between the page and the slot:

  vba = vblock * (pages_per_sb * planes * vbas_per_page)
      + page * (planes * vbas_per_page) + plane * vbas_per_page + slot

Untranslated, those VBAs land on unrelated and often erased pages, which is
why the context looked unusable. A run of consecutive CXT VBAs is only
contiguous here within one 4-slot group, so extents are split at plane
boundaries on import. The TREE is also partitioned by logical range across
the context superblocks, and only the oldest carries the BASE marker, so all
of them are merged rather than taking the first that parses.

Two quadratic paths are gone: whimory_range_update() walked the interval map
from rb_first() on every L2V update, and the packed L2V repacked an entire
root per update while collecting a root walked the whole map. A binary-search
lower bound plus deferring the pack until replay finishes took a full
brute-force recover from 1005 s to 504 s with byte-identical output.

Ranges now coalesce across weaves when they are contiguous in both LBA and
VBA, keeping the older weave so a later claim is never wrongly rejected as
stale. That cut 208134 interval nodes to 52352 and is what made a full replay
fit in 55 MiB of RAM.

Recovery is now a small state machine: a rebuild cannot tear down a live map,
and re-binding an already-registered disk no longer leaves the gendisk at
capacity 0 and fails every read.

Diagnostics are quiet by default; diag=1 restores the bring-up dumps.

PMIC
----
Reports that looked like kernel crashes were the d1830 100 ms I2C button
sweep misreading r7 during NAND activity, emitting KEY_POWER from a single
sample, and userspace powering the device off. Buttons are interrupt-driven
by default now (btn_poll_ms=0) with a confirming re-read (btn_confirm_ms).

DMA
---
s5l_pl080_desc_free() ran dma_free_coherent() after an in_atomic() test.
That test cannot see spinlock context on a non-preempt build, and the driver
openly leaked when it guessed wrong. Descriptors holding a coherent LLI block
are now queued to a workqueue; pool-backed ones are still released inline.
Compile-tested only: audio on this board is broken for unrelated reasons and
is the next work item.

Cleanup
-------
Cross-driver declarations were repeated as bare externs in six files and had
begun to disagree. They now live in include/linux/apple-n31.h.

A previous automated pass had mangled comments across these drivers, leaving
fragments such as a comment opening with a bare colon, and continuation lines
unindented at column 0. Twelve were rewritten as prose and roughly a thousand
continuation lines re-aligned; the resulting .ko files are byte-identical, so
that part is comment-only.

checkpatch on the touched files goes from 2 errors and 172 warnings to
0 errors and 142 warnings. What remains is deliberate: split format strings
and deep nesting in the NAND sequencer, both of which need real refactoring;
msleep values that come from hardware timing; and sysfs_emit false positives
where the trailing newline is inside a %s argument.

Verified on hardware: read-only FAT mount with 50 Fxx directories, 496 music
files, zero read misses and zero VFAT bread failures. An fsck of the volume
reports 702 files, 91 directories, 2.9 GB, clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Audio, two independent faults either of which is silence on its own.
GPIO 7 was never muxed: sub_BCB60 claims both IIS0 pads together on every
TX enable and releases both on disable, but pad 7 was being skipped in
the belief it drove the display. It does not -- the panel is driven
entirely from the LCDIF, no display code touches GPIO, and RetailOS holds
pad 7 at function 3 while playing music with the panel lit. And register
0x0227, the live output gain, sat at its bring-up minimum forever:
sub_D2C98 encodes code = dB from -50 to +12 and -50 + (dB+50)/2 below
that, floor -63 = -76 dB, which is where the bring-up sequence leaves it
and matches the -66 dBFS measured on glass. The mapping added here
reproduces sub_D2C98 exactly across its whole input range. The write also
sign-extended bit 6 into bit 7 instead of masking to seven bits as
sub_400330 does, and the mixer control only reached a software PCM scaler
that the DMA path never consulted. Volume and mute now drive the hardware
as Headphones Playback Volume/Switch.

IIS2 folded into s5l8740-i2s. The two ports share the audio clock gate at
CLKCON+0x30 and each wrote it directly, so stopping FM capture idled the
clock under music that was still playing; a per-port wanted flag now
idles it only when neither wants it. IIS2 also gains the pad group it
never had: sub_15DD5C claims GPIO 97/98/119 at function 2 on FM power-on
beside programming audio device 2 and kicking RXCOM, and releases them on
power-off. Those pins had been described as BCM control lines and given
to hci_bcm, which drove the capture bus as GPIOs.

I2C pads are muxed for the first time. Nothing in this port ever did,
which is invisible on a bus the bootloader leaves configured and fatal on
one it does not: i2c1 (PMIC) works while i2c0 (Tristar) reads as noise.
sub_5714EE gives the per-bus pairs and sub_1860 fixes the indices.

FM is named from BlueTool's own FM_RDS_Command definition rather than raw
hex, and gains a read path, RSSI, SNR, RDS group decoding for PS, RT, PI
and PTY, and raw register plus arbitrary-HCI passthrough for userspace.
ROUTE_PCM was only ever read back and never written, leaving the route to
the port IIS2 captures wherever the last owner left it.

Also: the touch download follows the stock tail and no longer interleaves
24 KB of diagnostic reads into the HBPP sequence; SPI2 engine setup has a
single owner again instead of two drivers programming different dividers;
the playback DAI link is nonatomic because its trigger reaches the codec
over SPI; screen sleep, LCD power cycling and PMU rail arbitration land,
with the rail-held mask stopping the global repair from dropping a rail a
driver is using -- the real cause of the white screen.

None of this is hardware-validated yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A volume that mounted with files missing, then would not mount, then
mounted but could not open files. Six defects, each found by measuring
rather than by reasoning about the previous one.

The VBA space was bank-major -- every (ce, cau, vblock) triple got its own
superblock index -- while the FTL native space puts the plane between the
page and the slot. CXT VBAs had to be translated on the way in, and a run
of consecutive ones was contiguous only within a 4-slot group. The seed
produced 236675 ranges for 938395 LBAs, under four LBAs per range. Matching
the native layout removes the translation and lets extents stay whole:
3137 ranges.

The CXT diff replay skipped every superblock on the volume. The skip tested
page 0's weave -- the oldest page in the block, written when it was opened
-- so a block appended to right up to power loss still tested older than
the checkpoint. It now tests the newest weave, taken from page 127, and
only when page 127 actually supplied one.

The VBA range check bounded by user_blocks, which is blocks_per_cau less
the VFL tail. That is the right bound for what may be allocated and the
wrong one for what a stored VBA may name; it discarded 81 CXT records for
vblocks 1987..1991, around 2000 LBAs. The VFL is an identity map over
blocks_per_cau, so that is the bound.

struct whimory_bte is {weave_seq_add, aux, lba, span} and 185 BTOC pages
carry an eight-byte header before the array. The parser read the header as
a record, got span=1266 against a 508-VBA superblock, broke on record zero
and declared the page unrecognised -- 127 pages and ~2032 LBAs per
superblock, 255 superblocks. That is what left directory sectors unreadable
on a volume whose FAT mounted fine. btoc_pages_valid 308 -> 493.

A BTOC with no BTE array, and a block no rule classified at all, are now
rebuilt from per-page meta instead of dropped. WHIMORY_SB_UNKNOWN had been
defined and never assigned: 78 blocks hit `continue` before nsb++, so they
never entered sbs[] and were never replayed.

Classify reads one 4112-byte record instead of a 16448-byte page. Slot 0
carries the data and meta it inspects, so a block settles there or
escalates; only an erased, zero or unrecognised slot-0 meta needs the full
page, which is ~80 blocks against ~2270. The pass went from ~57s to ~31s.

The FPart scan had never read the NAND. It called page_read with a meta
buffer, which is refused unless meta_dma_read is set -- and that is
deliberately off because a permanent live CS kick reboots the device. Every
read returned -EOPNOTSUPP: 512 reads in 60ms, and "sig=0, not a native
open" was a verdict about a region nobody had looked at. It uses cs_phys
inside a dma_session now, which needed an slc-capable entry point since the
FPart region is SLC and is found by trying plane 1 first.

Measured after: every file on the volume reads with no kernel error.
Playback ran for a few seconds and then took the whole device down rather
than just the audio.

The error latch was cleared and nothing else happened. The channel stayed
enabled, so whatever raised the error raised it again immediately and the
handler cleared it again. On a single core with the watchdog disarmed that
is an interrupt storm, which from outside is indistinguishable from a
lockup -- and it fits a failure that arrives after seconds of working
playback rather than at the first period. An error now disables the channel
before anything else, ends the descriptor, and counts; after eight the
channel is marked stuck and start() refuses it, because restarting a
channel that errors every time resumes the storm.

The handler also returned IRQ_HANDLED unconditionally, including when
neither engine had a bit set. That tells the kernel every interrupt on the
line was ours and dealt with, disabling the spurious-interrupt protection
that would otherwise notice a stuck line and mask it. It claims only what
it serviced now, so a fault costs the audio and not the device.

Not proven to be the cause -- the device was down and could not be tested.
Both are defects on their own terms, and both turn a recoverable audio
fault into an undebuggable one.

The cyclic LLI ring was checked and is correct: the last node wraps to
lli_phys, and the early-exit case where sg_left runs out before per_period
chunks leaves a dangling next-pointer that the fixup after the loop
corrects.
…ck gate

The codec produced silence. Several separate reasons, all from the
bootloader sequence rather than from guesswork about the part.

The analog stage is powered by the sequence the bootloader runs, which is
what produces the audible plop and 0x2F=0x80 ready=1. An earlier attempt to
raise an "analog LDO" through PMIC registers 21-23 was removed: those are
decimal 0x14-0x17, and bit 4 is the top bit of a 5-bit voltage field rather
than an enable, so it put +400mV on three rails and locked the device twice.
The decomp writes (code & 0x1F) only.

Jack detect is gone entirely, from both the prepare path and the play
latch. This board does not use the CS42 jack detect, and gating playback on
it meant the codec was configured and then never allowed to run.

Rate handling: an automatic rate path with an SRC for rates the hardware
does not take, so an unsupported rate degrades instead of failing.

force_stock_audio_parent is off. It clobbered the SoC CLKCON and took the
NAND down with it, which read as a storage fault.

Stage markers through the graph bring-up (write_table, settle, settled ok,
write 0x500, read 0x528, verify) so a hang inside it can be placed rather
than inferred, and cancel_delayed_work_sync in the stop path became
cancel_delayed_work -- the sync form deadlocked against the work it was
waiting for.
bcm2078-bt is built in and probes around t=2.4s; the PMIC driver is a
module userspace loads at about t=7.1s. A driver asking for power through a
bespoke hook in that window gets -ENODEV and has no way to wait, so the
controller simply never came up and hci0 timed out on 0xFC18.

The rail is exposed as a regulator instead, with of_match and
regulators_node so a device tree node can name it. That makes the kernel do
the waiting: devm_regulator_get returns -EPROBE_DEFER until the PMIC
registers, and the consumer is re-probed. It does not fit n31_pmu_rails[],
which describes single-register LDOs at 0x17..0x21 -- this one is 0x57 bits
7:6 and 0x58 bit 0 plus 6:4 -- so it carries its own descriptor whose
enable and disable defer to d1830_bt_rails().

The UART-to-HCI bridge stays dumb: powering the chip belongs to the chip
driver, and the bridge only carries bytes. REG_ON on GPIO 97 was never
being asserted; gpio_poke covers that.

Also here: the PMIC button poll is load-bearing and stays at 100ms.
/proc/interrupts has no PMIC nIRQ -- of_irq never maps GPIO 86 -- so the
poll is the mechanism, not a redundant product-behaviour poll. Disabling it
on that reading broke Home, Sleep and Play.
…s demux

The SPI engine has been seen with two different status encodings. The
driver waited on the ROS masks and then, on timeout, accepted the other
family -- which turns a genuinely stuck transfer into a slow success and
hides it. The encoding is a per-instance property now: AUTO latches on the
first transfer and only the latched family is consulted afterwards, so a
timeout is a timeout. The spin budget came down from 500000, which is less
a timeout than a way to make a stuck engine look like a hang; more than one
"lockup" chased here was exactly that.

Nimbus reads a report whenever ATTN is asserted rather than requiring the
ping to have succeeded first, honours the length the firmware declares
instead of a fixed size, and burst-transfers rather than byte-banging.
Bootloader status words are recognised as such, 0x4f81 among them, so "the
application never started" is distinguishable from "the part is not
answering at all".

MikeyBus was discarding pkt[2], which is the channel -- the remote stream
is channel 4 and was being parsed as though it were something else. It is
demuxed by channel now and registers an evdev node.

The button map is a placeholder and is marked as one in the source. The
decomp gives the vocabulary from the handler names -- HandleMikeyCenter,
HandleMikeyVolumeUp, HandleMikeyVolumeDown, HandleMikeyAllUp -- and that
last one is the useful inference, since an "all up" event means the byte is
a bitmask of held buttons rather than a button code. What the decomp does
not give is which bit is which, so the ordering is a guess and says so.
gpio-s5l8740 gains the button wiring and a release timer, and keeps the
Bluetooth power control that some board variants route through it behind a
switch that is off: the VIC routing is known but the EIC level semantics
are not, and asserting a rail on a guess about edge polarity is not
something to leave enabled by default.

The LCD driver takes CLKCON with devm_ioremap rather than
devm_ioremap_resource. The region is shared with the clock controller, so
requesting it exclusively made whichever driver probed second fail -- which
presented as the display working or the clocks working depending on probe
order.

Tristar carries the OSOS sub_11C8C ID/VBUS/CONDET reading, read-only with
the status poll off. The 3.5mm path on this board is CS42 plus MikeyBus and
not a Dx write, and the v36 bits that are still unmapped say so rather than
being given a plausible meaning.

include/linux/apple-n31.h is the interface the PMIC, GPIO, DMA, NAND and
touch drivers share; it grows the rail and bring-up entry points the
Bluetooth and touch work needed.

A note on the canonical copy of these files: the tree under n7-upstream is
a build artifact. rebuild-linux-gate0.sh syncs it from tools/linux-n31, so
an edit made there is overwritten on the next build and the change appears
to have had no effect. This tree and tools/linux-n31 are the sources.
The PMIC node gains a regulators subnode holding the bt rail, and the
Bluetooth nodes gain the supply properties that make the ordering work:
bt-supply on the companion and vbat-supply on the controller. Without a DT
node naming it, the regulator is registered but unreachable, and the
consumer has nothing to defer on.

panic=-1 became panic=0. Rebooting instantly on panic meant every oops
scrolled off with the reboot and the only symptom left was a device that
came back up, which cost real time on the scheduling-while-atomic fault in
the SPI wait. Halting leaves the trace on the glass.

The nodrm variant is resynced against the main DTS; it had drifted far
enough that the two described different hardware in places.
The volume mounted its boot sector and then failed every directory walk:

    FAT-fs (s5l8740-ftl): error, fat_get_cluster: invalid cluster chain

The wrong BPB candidate was selected and the whole volume sat six sectors
out. n31_validate_fat_critical() checks that its nine critical sectors
*read*; it never looks at what came back, so both candidates scored 9/9:

    BPB_CAND freemyipod#1 fmss_lba=49285 weave=..ad crit=9/9   <- selected, newest
    BPB_CAND #2 fmss_lba=49279 weave=..ac crit=9/9

49285 - 49279 = 6, and the FAT belongs to the older one. Sector 0 still
read as a valid BPB and sector 1 as valid FSInfo because both are found by
content and are static. Dumping the FAT region showed the tell: a
well-formed FAT32 chain stepping 0x400 entries per 4096-byte sector,
starting six sectors early, so vfat began its walk mid-table.

Every FAT32 opens entry 0 with the media descriptor in the low byte and the
EOC nibbles above it, and entry 1 all ones. n31_fat_first_sector_ok() reads
L->fat_start and checks exactly that, masked to the 28 bits FAT32 entries
carry, against the media byte in the BPB at offset 0x15. It gates both the
"perfect critical set wins immediately" early-out and the scoring below.

    bpb_try fmss=49285 crit=9/9 fatsig=0
    bpb_try fmss=49279 crit=9/9 fatsig=1
    fat_base_lba=49279 valid=1 selected=2
    MOUNT_OK -> Apps  iPod_Control  n31os  System Volume Information
    496 files under iPod_Control/Music, full-file reads, 0 errors

n31_fat_semantic_validate() is declared and never called, which is why the
bpb log has always printed itunesdb=0 music_dirs=0 -- stale zeros, not
findings. It would have caught this too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ON+0x30

s5l8740_audio_clk_set() did

    writel(any ? CLKCON_AUDIO_PLAY : CLKCON_AUDIO_IDLE, clkcon + 0x30);

on every play and every stop. Those constants (0x32190 and 0x1c20) are
whole-register snapshots captured from RetailOS at two moments, so each
write imposes the entire captured clock state of the SoC, including
whatever every other peripheral was doing when the snapshot was taken.
CLKCON+0x30 carries gates that have nothing to do with audio.

The NAND controller is one of them. After enough play/stop transitions the
FMSS reads back FMCTRL0=0 NANDSTAT=0, sub_10453C times out on FMCTRL1 bit
30, and Whimory open fails with -110:

    s5l8740-nand: 10453C FMCTRL1 bit30 timeout v=00000000
    s5l8740-ftl: Whimory open failed (-110) - NOT registering
                 /dev/s5l8740-ftl (fil=0 sig=0 vfl=0 ftl=0 l2v=0 lba0=0)

The raw CS path still worked, so the NAND itself was fine -- it was the
controller clock, and storage stayed dead until the next boot.

Stock never writes this register wholesale. sub_41CBD8 sets or clears one
bit and preserves the rest. So restrict the write to the bits that differ
between the two snapshots and preserve everything else from the live
register. Confirmed on hardware: FMCTRL0=0x1 NANDSTAT=0x62, FIL_Init OK.

Also in this change:

  - sub_B6620(port, 0) is *(base + 8) |= 6, an OR. The DMA arm wrote a bare
    0x6 and cleared every other bit in TXCOM. Equivalent while TXCOM reads
    0 first, which it does today, but it silently drops any bit something
    else sets; the same function ORs 0x6 into RXCOM on the capture side.

  - s5l8740_codec_clk_gate(): sub_4F82F8() returns 9 and case 9 of
    sub_41CBD8 is CLKCON+0x0C bit 15, active low. D3280(1) drops that gate
    and D3280(3) restores it, which is what the codec 0x0006/0x0007 bit-6
    freeze latch brackets. Exported for the codec driver, which owns the
    state machine but not the CLKCON mapping.

  - The txcon module parameter now writes IIS0+0x04 when set. Sweeping it
    used to require defeating the "already programmed" check with clkdiv,
    which re-ran the pad mux and the CLKCON writes on every play -- which
    is how the NAND got gated and how the device wedged mid-stream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine defects on the prepare/play path, found by decoding sub_D3280,
sub_D2F64, sub_D34C0 and sub_183138 properly rather than transcribing the
bootloader, and by checking every register this driver writes against every
write in the RetailOS image.

The state machine was inside out:

  - The analog power-up was the bootloader's sub_1310. OSOS has its own,
    sub_D3280(1), differing in five registers -- 0x0225 = 0x33 not 0x19,
    0x0220 mask 0x78 = 0x78 not 0x50, plus 0x0229 and 0x0075 writes the
    bootloader never makes -- and it leaves 0x0007 bit 6 set where the
    bootloader leaves it clear.

  - States 1 and 3 ran in the wrong order. They are a matched pair around
    the codec clock gate: state 1 takes the freeze latch and stops the
    clock, state 3 restarts it and releases the latch. Calling state 3
    first released a latch nothing had taken, and state 1 then took it with
    no one left to release it. 0x0075 bit 7 inverted the same way.

  - state_4_output_on() is a power-DOWN and ran as the last step of every
    prepare. sub_3C6244 settles it: a dB-code converter, fed codes 0x40 and
    0x41, i.e. -90 dB and -76 dB. It also cleared the analog enable state 1
    had just polled 0x002F for, dropped the 2v5 rail, wrote 0x0225 = 0x00
    over state 1's 0x33, and clobbered every 44.1 kHz stream's SRC pair
    with the 48 kHz native values. Renamed and moved to the stop path.

Hardcoded results of computed functions:

  - sub_D2F64 is computed. Mode 271 ("on") applied mode 6's ("off") value to
    0x000D, and mode 6 drove 0x0006 bit 2 the same way mode 271 does when
    stock drives them oppositely.

  - sub_D34C0 is a three-way branch on MEMORY[0x892A038], not a sequence.
    This ran the sub_183138 body and another arm's tail every time; they
    program different blocks for the same job (0x010B/0x010C vs
    0x0223/0x0224), and the tail released a hold 183138 had just raised.
    183138 ends muted with the 0x0220 bit-5 hold set by design -- sub_D2F64
    is what releases it -- so prepare must run set_rate before
    output_path_enable, and does.

Work that did not belong on the play path:

  - post_iis_start() wrote 0x0229 = 0x41 and 0xC96F = 0x0E, both standby
    values from D3280(4). 0x0229 is written by sub_D3280 and nothing else
    in the whole image, and 0xC96F = 0x0E is the 2v5 rail *down*.

  - asp_lock() re-ran the entire rate programming on every start, and
    asp_clock_pulse() ran its 0x0220 bracket backwards so it finished in
    idle. Both left the part muted and held at play time.

  - Setting the volume forced the mute state and set the graph commit bit.
    Stock's volume path is sub_D2C98 then sub_400330: gain and nothing
    else. Split into apply_user_vol() and apply_mute().

  - The graph is 80 SPI writes and a 100 ms settle -- 118 ms measured -- and
    ran inside the transport START callback, between the application asking
    for playback and the DMA being kicked. The stream underran and ALSA
    restarted it, giving three STARTs in 260 ms. Built in hw_params now;
    START went from 118 ms to 2 ms.

Invented code, removed:

  - The ASP lock. 0x002F is read exactly once in the whole image (the
    readiness poll in sub_D3280(1), testing bit 7) and bit 6 is never
    examined anywhere. This was five attempts of eighty polls on bit 6,
    with a module parameter to flip a polarity that had never been
    established -- up to two seconds per playback start on a bit stock does
    not look at.

  - cs42_hsdet_pulse(). Credited to "RE D3280(3)/audio_on" and from
    neither; nothing in the image writes 0x0073, 0x0079 or 0x0009. It left
    0x0009 -- MCLK control -- rewritten rather than restored.

  - All codec headset detection, by request: force_headset, jack_poll_ms,
    the poll work and its mid-stream 42D364(0) teardown.

Settled and left alone: the IIS0 TX port object sets a3 = 1, so sub_BCB60
takes its a3 != 0 branch and TXCON = 0x03100099, RXCON = 0x1000 and pads at
function 3 are stock-exact -- matching the live pad readback. The static
graph table matches the image position-for-position across all 80 writes.
sub_174E7C's tap formula matches including the +2/+1 and the 160 divisor.

Not yet producing sustained sound: the IIS0 transmitter still takes one
32-byte burst and stalls with STATUS=0x424. Every register value now
matches stock, so what remains is sequence, not content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carried along with the storage and audio work of this session; no
behavioural change intended beyond what those commits describe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants