The nitty gritty. See README.md for the one-page intro.
- Building from source
- The module_layout CRC patch
- Running it manually
- What's in each MMIO section
- Slim variant deep dive
- Known limitations
- What's NOT in the JSON
For the kmod you need a kernel build tree matching the running kernel.
JoeyEamigh/spotify-kernel
ships the Car Thing's kernel-common source with superbird_defconfig
and a crosstool-NG-built GCC 6 toolchain that matches the original
stock build.
git clone --depth 1 https://github.com/JoeyEamigh/spotify-kernel.git
cd spotify-kernel
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
HOSTCFLAGS="-fcommon -Wno-error" \
KCFLAGS="-Wno-error" \
superbird_defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
HOSTCFLAGS="-fcommon -Wno-error" \
KCFLAGS="-Wno-error" \
-j$(nproc) vmlinux modules
# vmlinux link may fail at the very end (undefined of_resolve_phandles
# in aml_bl_load_overlay) — that's harmless. Module.symvers is what
# matters and it's already been generated by modpost.Then build both kmods against it:
cd /path/to/peek-carthing
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
KDIR=/path/to/spotify-kernel
aarch64-linux-gnu-strip --strip-unneeded peek_mmio.ko peek_mmio_slim.koThe default Makefile has obj-m := peek_mmio.o peek_mmio_slim.o, so
one invocation produces both .ko files.
spsgsb/kernel-common works too, with the same flags. The key
requirement is that Module.symvers exists in the kernel tree before
you build the module (run a full make modules once).
This is the trick that makes the kmod loadable on the stock kernel:
out-of-tree builds produce a module_layout CRC that doesn't match the
stock kernel's, but we can post-link-patch the __versions section to
use stock's known-good CRC (harvested from mali.ko):
import struct, subprocess
KO = "peek_mmio.ko" # or "peek_mmio_slim.ko"
subprocess.run(["aarch64-linux-gnu-objcopy", "--dump-section",
"__versions=/tmp/v.bin", KO], check=True)
data = bytearray(open("/tmp/v.bin", "rb").read())
for i in range(0, len(data), 64):
name = bytes(data[i+8:i+64]).split(b"\x00", 1)[0].decode("ascii", "replace")
if name == "module_layout":
struct.pack_into("<I", data, i, 0x8b79d6c0) # stock kernel CRC
open("/tmp/v.bin", "wb").write(bytes(data))
subprocess.run(["aarch64-linux-gnu-objcopy", "--update-section",
"__versions=/tmp/v.bin", KO], check=True)This step is not needed if you're loading the module on a kernel you built yourself (CRCs will already match).
adb push peek_mmio.ko /tmp/peek_mmio.ko
adb push peek-collect.py /tmp/peek-collect.py
adb shell python3 /tmp/peek-collect.py > hwinfo.jsonIf you want just the raw dmesg dump (no JSON aggregation):
adb shell insmod /tmp/peek_mmio.ko
adb shell 'dmesg | grep peek_mmio:'The module returns -EBUSY from init by design so it doesn't stay
loaded — it's a one-shot dumper. The cost: lsmod will show it as
[permanent] Loading until the next reboot, and re-running insmod
gets "Device or resource busy". The python aggregators handle this
automatically (they re-parse dmesg from the previous run).
The full kmod dumps everything below; the slim kmod skips the rows marked (slim drops).
| section | addr | size | what's inside |
|---|---|---|---|
| HHI clock controller | 0xff63c000 |
1 KiB | All G12A PLLs, video clock dividers, MIPI analog DPHY bias at +0x000..0x008 (MIPI_CNTL0/1/2), tsensor clk, MIPI DSI phy clk |
| MHU mailbox (slim drops) | 0xff63c400 |
128 B | SCP coprocessor IPC (usually idle = all zero) |
| DMC / DDR controller (slim drops) | 0xff638000 |
512 B | Per-byte-lane DLL training at +0x180..+0x1f0 (different across RAM vendors) |
| Mali Bifrost GPU | 0xffe40000 |
256 B | GPU_ID, shader count, power state. Decoded → product name |
| eMMC controller | 0xffe07000 |
256 B | SD_EMMC_CLOCK @ +0x00, SD_EMMC_START/CFG/STATUS/IRQ_EN @ +0x40..+0x4c — live bus speed, mode, bus width |
| Audio bus (slim drops) | 0xff642000 |
512 B | Live PDM mic TODDR ringbuffer addresses in DRAM — write pointer advances real-time |
| t9015 audio codec (slim drops) | 0xff632000 |
256 B | Internal analog DAC (unused on Car Thing — no jack) |
| PWM AB | 0xffd1b000 |
64 B | Backlight PWM channel A |
| PWM EF (slim drops) | 0xffd19000 |
64 B | Unused PWM block |
| AO bus PWM (slim drops) | 0xff802000 |
64 B | Separate AO-domain PWM block |
| VPU near-ENCL | 0xff907000 |
1.25 KiB | Display timing: htotal/vtotal/hsync/vsync, framebuffer pointer, OSD compositor state |
| VPU VIU_OSD1 | 0xff906840 |
576 B | OSD1 CTRL_STAT + MATRIX area (word 0x1a10..0x1a9f) |
| DSI host | 0xffd07000 |
1 KiB | DWC DSI v1.21 host + Amlogic TOP wrapper |
| MIPI digital DPHY | 0xffd44000 |
256 B | (usually all zero — clocking is HHI-side on G12A) |
| Pinmux / GPIO state | 0xff634480, 0xff634440 |
512 B each | Pad function select + live GPIO levels |
| SARADC | 0xff809000 |
64 B | Raw ADC regs (decoded values via IIO sysfs) |
| USB2 PHY21 (slim drops) | 0xff63a000 |
small | USB phy state + tuning |
| USB2 PHY (slim drops) | 0xffe09000 |
small | USB phy state + tuning |
| DWC3 USB3 controller (slim drops) | 0xff500000 |
256 B | USB3 events, GSTS, link status |
| Reset controller (slim drops) | 0xff634404 |
64 B | Per-peripheral reset state |
| Tsensors (slim drops) | inline | 16 B | PLL + DDR thermal sensor raw cfg/stat |
The slim kmod's section labels and dmesg prefix (peek_mmio:) are
identical to the full kmod's, so the parsers in either python script
handle either kmod transparently.
Compared to the full kmod, peek_mmio_slim.ko drops MHU, DMC / DDR
controller, audio bus, t9015 codec, PWM EF, AO bus PWM, both USB2 PHYs,
DWC3 USB3, reset controller, and tsensors. Net: about half the dmesg
footprint.
Stripped, peek_mmio_slim.ko lands around 7 KB on disk vs. ~99 KB for
the unstripped full kmod (most of the size difference is debug info,
not text).
meta tool / version / timestamp + canonical device serial
+ raw_encoding (int-array | hex-blob)
system uname, kernel banner, /proc/cmdline, hostname, uptime
soc MIDR-decoded CPU info, on-die serial, features
serials every serial source consulted, canonical pick + provenance
emmc every mmc host + CID/CSD/EXT_CSD + controller MMIO (kmod)
+ boot_partition_sizes (catches the 2 MiB vs 4 MiB variant)
gpu Mali sysfs + decoded GPU_ID from kmod (Mali-G31 r0p0 etc)
display DRM debugfs (mode/fb/connectors/EDID) + decoded panel
timing + MIPI bias + DSI host + PWM AB (kmod)
panel_id TLSC6X probe → BOE / Wily / Holitech + vendor_id / hw_id
board_rev SARADC channel 1 mV (via _input) + all 8 channels for
context (PCB / BT / DRAM NTCs etc.)
The kmod is optional for the slim flavor — userspace-only paths
still produce ~80 % of the fingerprint. If peek_mmio_slim.ko (or
peek_mmio.ko) is loaded, its dmesg is parsed and filtered down to
just the slim topics.
peek-slim.py integrates a TLSC6X-based panel-variant probe. It talks
to the touch controller at I²C 0x2e via I2C_SLAVE_FORCE, which claims
the slave even while the kernel touch driver is bound. The sequence:
- Write 6-byte DMA-enable command to reg
0x42bd; poll reg0x0001until the chip echoes its own address and reports DMA mode = 1. - Sniff
MCCODEfrom reg0x8000(or0x0009fallback) to pick which config slot to read (0xd6e0vs0x9e00). - Read 204 bytes of config NVM, extract
vendor_id = (ver >> 9) & 0x7fandhw_id = u16 at byte 106. - Restore normal touch-reporting mode by writing
0x05to reg0x03(the kernel driver'stlsc6x_set_nor_mode_nolocksequence).
Net effect: kernel touch driver undisturbed, no unbind needed.
| vendor_id | hw_id | variant | panel_type |
|---|---|---|---|
| 0x11 | 0x065c | BOE | lcd_8 |
| 0x70 | 0x2d5c | Wily | lcd_9 |
| 0x11 | 0x0d5c | Holitech | lcd_10 |
The slim aggregator has two size-reduction flags:
python3 peek-slim.py > h.json # ~45 KB (indented)
python3 peek-slim.py --compact > h.json # ~20 KB (no indent + hex blobs)
python3 peek-slim.py --gzip > h.json.gz # ~4 KB (compact + gzip)--compact does two things:
- Drops JSON indentation.
- Repacks each MMIO block's
rawrows from a dict-of-int-arrays ({"0x0040": [w0,w1,w2,w3], ...}) into a single big-endian-ordered, space-separated hex string. Trivial to slice back to rows.
The meta.raw_encoding field records which encoding you're looking at
("int-array" for the default, "hex-blob" for compact/gzipped).
--gzip implies --compact and writes binary to stdout.
- Stock kernel
CONFIG_MODULE_FORCE_LOAD=n— IGNORE_MODVERSIONS viafinit_modulereturns-ENOEXEC. The CRC patch above is the only path that works. - AO_SEC_SD_CFG / AO_SEC_GP_CFG are intentionally skipped —
they're secure-world-only on G12A and reading from EL1 = sync abort
- kernel oops. The boot source / reboot reason fields live in
/proc/cmdlinevia bootargs Spotify u-boot injects.
- kernel oops. The boot source / reboot reason fields live in
- eFuse SMC reads are skipped — stock BL31 doesn't respond to
vendor SMC IDs
0x82000030/0x82000041, calling them hangs the CPU and leaves the kmod permanently stuck. Getusid/f_serialfrom/proc/cmdlineor viafastboot oem run printenvif you're running a custom u-boot. /dev/memis disabled on the stock kernel (CONFIG_DEVMEM=n), which is why we need the kmod at all.
Get these via separate tools:
- Vendor BL2 / FIP body bytes — use
superbird-fip-tools/aml_decrypt.py. - eMMC EXT_CSD bytes —
cat /sys/kernel/debug/mmc*/mmc*:*/ext_csddirectly works (we only print the path hint in the JSON to avoid blocking on the host mutex).