olragon

A 2016 TV box, mainline Linux, and five days of evidence

🌿 buddingupdated 2026-10-14#linux#hardware#tv-box#mainline#reverse-engineering#field-notes

Field notes, written while the box still sits on the bench. Every claim below has a measurement, a log line or a screen grab behind it. Where I only inferred something, I say so.

Tiếng Việt: Hộp TV 2016, Linux mainline, và năm ngày gom bằng chứng.

The Q10 Pro mainboard: purple PCB, Hi3798CV200 under a black heatsink, AP6255 WiFi module below it
The patient: board v2.2, Hi3798CV200 (4× Cortex-A53) under the heatsink, 2 GB RAM, AP6255 WiFi/BT. Serial sticker blurred.

The box, and the question

The HiMedia Q10 Pro is a 2016 "audiophile" Android TV box: a quad-core HiSilicon Hi3798CV200, 2 GB of RAM, a 3.5" SATA bay, a 16-segment VFD clock on the front, RCA + optical + HDMI audio. Its last firmware is Android 7 on a vendor 4.4 kernel.

The first question was whether to chase a newer Android or go to Linux. The answer turned out to be structural, not a matter of effort:

So: mainline Linux, and accept that every media driver has to be ported or written.

Booting blind: a watchdog as a telemetry channel

There is no serial console on this box, and there was no screen output for the first two days. A kernel that hangs and a kernel that boots fine look the same: a black TV and no ping.

What worked was the SoC's own SP805 watchdog. Each checkpoint in early boot re-arms it with a different timeout, so the time until the box resets names the last checkpoint it reached. Over seven flashes and 18 checkpoints:

reset cyclelast checkpointwhat it proves
~19 sstextthe kernel proper is entered
134 s__create_page_tablesthe whole pre-MMU path is done
194 s__mmap_switchedMMU on, running from virtual addresses
216 skernel_init_freeableevery initcall ran; about to exec /init

Things that bit me, each with a lesson that carries over:

The network came up once two device-tree facts were fixed. The Ethernet PHY's reset controller was built as a module (=m) in an initramfs that loads no modules, so the PHY was never reset. And the MDIO bus scans only direct children, so the PHY node had to sit straight under the MAC.

What works now

All of it measured on the box, kernel 6.18.55 with my patch series (k14 to k25 builds). Screens are read from the display controller's scan-out buffer in RAM, so they show what the HDMI port sends.

TV boot menu: Q10 Pro, choose a session — Desktop, Kodi, Android 13, Original Android 7
The boot menu on the TV, driven by the remote or the front touch keys.
partstatusevidence
4 CPU coresworkssecondary cores released through the TEE (SC_GEN1); the vendor's SRAM path isn't writable from the non-secure world
RebootworksSP805 restart with LOAD 0x100: HiSilicon's clone never fires from 0. Back in 65 s
CPU DVFS + GPU devfreqworks840–1150 mV CPU voltage scaling, GPU 200–400 MHz
Ethernet, WiFi (AP6255)worksWiFi needed GPIO4_6 (WL_REG_ON) plus 1.8 V SDIO pads
Side USB 3works91.7 MB/s SuperSpeed measured
IR remote + front touch keysworksthe touch board sends NEC codes into the IR receiver; the stock kernel never scans keys
Front VFD clock (TM1629A)works6× 16-segment font calibrated from photos; reads "HIMEDI" correctly
Display (VDP DRM driver)worksadopts the bootloader's 1080p60; G0 graphics + V0 NV12 video planes
GPU (Mali-T720, Panfrost)workskmscube 59.9 fps, glmark2 score 90 on KMS, 0 GPU faults
Hardware video decodeworksH.264, HEVC (8/10-bit, 4K), VP9 bit-exact; MPEG-2 within 66–68 dB
Audio (written from scratch)worksRCA, optical, HDMI and a USB DAC; heard on all
HDMI-CECTX worksRX untested: no CEC-capable TV here
SATAmarginalsee "What doesn't" below

Hardware video decode: from solid green to bit-exact

The decoder (VDH) and post-processor (VPSS) came from a community 6.12 port and needed real fixes for 6.18 on 32-bit. The first decode "succeeded": 300 frames, no errors, every frame solid green.

A solid green frame
Before: the VDH ran, the IRQ fired, the capture buffer stayed all-zero NV12. Green.

Two bugs were stacked on top of each other. The VDH flagged an error on exactly the 40 pictures that contain emulation-prevention bytes, which was fixed by letting the driver strip them. Then every frame failed in VPSS with post-processing failed, state=0x8d, fixed by non-secure writes plus a real write-back buffer instead of physical address 0.

ffmpeg testsrc2 frame 150 with burned-in timecode 00:00:05.000
After: frame 150, exactly the test pattern with its burned-in "00:00:05.000 / 150".

Then I checked it properly. The md5 of every decoded byte is identical across FFmpeg-hardware, GStreamer-hardware and FFmpeg-software (720p 461d0f24e55e…, 1080p 69914be4d1c3…). The hardware isn't "close"; it's the reference output.

clip (FFmpeg v4l2request)framesvs softwarefps
H.264 1080p602400/2400md5 identical75
HEVC 1080p 8-bit300/300md5 identical51
HEVC Main10 1080p300/300md5 identical (P010)31
HEVC 2160p (4K)150/150md5 identical15
VP9 1080p300/300md5 identical52
MPEG-2 1080p / 576i300 / 250min frame PSNR 68.4 / 66.1 dB41 / 68

Two performance findings worth knowing if you work on any similar SoC:

kmscube's coloured cube rendered by the Mali-T720
Panfrost on the Mali-T720, composited through the VDP: kmscube at 59.9 fps with vsync.

One unresolved oddity: software-decoded frames pushed through mpv's GL renderer raise Panfrost INSTR_INVALID_PC job faults (356 per 720p clip, reproduced). One such run preceded an unexplained reset. Hardware-decoded dmabufs through the same renderer cause 0 faults. I haven't investigated the Mesa side.

Audio: a driver written from register recipes

There is no mainline driver for the audio block (AIAO). I extracted a register-level recipe from the vendor SDK and wrote a small ALSA driver: one ring buffer read by all three ports, and an hrtimer-polled read pointer, so it needs no interrupt and no device-tree node. The tone test measured 48,592 and 48,593 frames/s on two ports (target 48,000, with a 1 s window plus process overhead). Then came the part that counts: the tone was heard on RCA, then on HDMI (N=6144, measured CTS 148,351), and the optical port lit up.

On top of that sits an "audio mode" that turns the box into a network streamer: MPD, AirPlay 2, Spotify Connect, UPnP/OpenHome, and bit-perfect output to a USB DAC (16/48 passthrough measured, 0 underruns). There is also output selection, per-port switches and L/R trim, from the remote, the CLI or a phone browser.

Music mode idle screen: Ready, play from AirPlay, Spotify Connect or a UPnP app, or browse the library in a browser; output Onboard (HDMI / RCA / optical); volume 8
Music mode waiting on the TV: AirPlay, Spotify Connect, UPnP, or the web library. The LAN address is pixelated.
Now playing screen: 01 Tone 96k, FLAC 24/96 to onboard, resampled to 48 kHz
The now-playing line tells the truth: the onboard path only does 48 kHz, so 24/96 is shown as resampled.

One box, several lives

The boot stick's kernel stays fixed as the fallback. It kexecs into the newest kernel under an armed watchdog with a failure counter, so a bad kernel costs one reset, never a dead box. The TV menu then picks a session:

LXQt desktop with a Windows-style start menu listing Chromium, Kodi, Android 13
Desktop: LXQt with a familiar start menu, GPU-accelerated through glamor (293 MB, near-idle CPU).
Android 13 home screen at 1:04, Mon Oct 12
Android 13 (LineageOS 20 in Waydroid), full screen on the Mali GPU: launcher in 100 s from the SATA disk, 724 MB used, pause/resume in ~1 s.
Kodi 21.2 Omega splash
Kodi 21.2 on GBM + GLES with the V4L2-request decoder.

Getting Android 13 up meant a string of silent failures, each fixed in turn:

What doesn't (yet)

Being honest about the gaps:

Mistakes I made, kept on purpose

These cost real time, so they stay in the notes:

Where it stands

Five days in, it's a quiet Linux box. It boots to a TV menu in ~90 s, decodes video in hardware bit-exactly, plays music to a DAC, runs a desktop, Kodi, Chromium and Android 13, and shows the time on its old VFD. The last physical fault is waiting on a new cable.

Tended by papa dragon & AI: the seed (the box, the questions, the "is that really true?") is mine; the long tending nights were shared.