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 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:
- Android 7 is the ceiling. The video, HDMI and GPU blobs are built against the vendor's 4.4 BSP, and Android's HAL is their only consumer. A newer Android needs a newer vendor BSP, and HiSilicon has left the set-top-box market. That BSP will never ship.
- A "vendor kernel + Debian" hybrid is a trap. The blobs speak HiSilicon's private MSP API, not V4L2 or DRM. Kodi, mpv, ffmpeg and Chromium can't talk to them. You'd keep the drivers and still have no hardware video.
- Mainline is current. Linux 6.18 is a supported longterm; kernel 4.4 hasn't been on kernel.org's list for years.
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 cycle | last checkpoint | what it proves |
|---|---|---|
| ~19 s | stext | the kernel proper is entered |
| 134 s | __create_page_tables | the whole pre-MMU path is done |
| 194 s | __mmap_switched | MMU on, running from virtual addresses |
| 216 s | kernel_init_freeable | every initcall ran; about to exec /init |
Things that bit me, each with a lesson that carries over:
- The overhead isn't constant. It's 6 s for a tiny stub but 11–14 s for a 12 MB kernel read off USB, and it grows with depth. With timeouts only 4 s apart, one reading fit three different conclusions. The tool now refuses any profile with gaps of 10 s or less.
- The last checkpoint hides everything after it. Nothing pets the dog, so a kernel that reaches userspace resets exactly like one that died right after the last checkpoint. The deepest checkpoint needs the longest timeout.
- A probe that can't fire isn't a probe. My "PID 1 ran" checkpoint opened
/dev/mem, and the initramfs had no such node. "PID 1 ran" and "exec failed" gave the identical 216 s. - One missing file can silence a whole script.
init.shopened withmount -t devtmpfs dev /dev 2>/dev/null./dev/nulllives on devtmpfs, so it didn't exist yet. POSIX says a failed redirection skips the command, and the script carries on. Every later2>/dev/nullline failed the same way, and the whole second stage was a silent no-op on a healthy kernel. Proof in one line:sh -c 'echo X 2>/no/such/dir; echo continues'prints onlycontinues.
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.

| part | status | evidence |
|---|---|---|
| 4 CPU cores | works | secondary cores released through the TEE (SC_GEN1); the vendor's SRAM path isn't writable from the non-secure world |
| Reboot | works | SP805 restart with LOAD 0x100: HiSilicon's clone never fires from 0. Back in 65 s |
| CPU DVFS + GPU devfreq | works | 840–1150 mV CPU voltage scaling, GPU 200–400 MHz |
| Ethernet, WiFi (AP6255) | works | WiFi needed GPIO4_6 (WL_REG_ON) plus 1.8 V SDIO pads |
| Side USB 3 | works | 91.7 MB/s SuperSpeed measured |
| IR remote + front touch keys | works | the touch board sends NEC codes into the IR receiver; the stock kernel never scans keys |
| Front VFD clock (TM1629A) | works | 6× 16-segment font calibrated from photos; reads "HIMEDI" correctly |
| Display (VDP DRM driver) | works | adopts the bootloader's 1080p60; G0 graphics + V0 NV12 video planes |
| GPU (Mali-T720, Panfrost) | works | kmscube 59.9 fps, glmark2 score 90 on KMS, 0 GPU faults |
| Hardware video decode | works | H.264, HEVC (8/10-bit, 4K), VP9 bit-exact; MPEG-2 within 66–68 dB |
| Audio (written from scratch) | works | RCA, optical, HDMI and a USB DAC; heard on all |
| HDMI-CEC | TX works | RX untested: no CEC-capable TV here |
| SATA | marginal | see "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.

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.

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) | frames | vs software | fps |
|---|---|---|---|
| H.264 1080p60 | 2400/2400 | md5 identical | 75 |
| HEVC 1080p 8-bit | 300/300 | md5 identical | 51 |
| HEVC Main10 1080p | 300/300 | md5 identical (P010) | 31 |
| HEVC 2160p (4K) | 150/150 | md5 identical | 15 |
| VP9 1080p | 300/300 | md5 identical | 52 |
| MPEG-2 1080p / 576i | 300 / 250 | min frame PSNR 68.4 / 66.1 dB | 41 / 68 |
Two performance findings worth knowing if you work on any similar SoC:
- Uncached capture buffers are a trap. 1080p is coded 1088 lines high, so GStreamer crop-copies every frame. From uncached memory that cost 28.7 s of CPU per 300 frames and capped playback at 15 fps. Making the buffers cacheable gave 94 fps at 3.6 s, 6.1× faster with 7.9× less CPU, and the md5s stayed identical.
- The display path matters more than the decoder. Through a CPU-copied framebuffer (simpledrm), 1080p played at 11.7 fps using 34 s of CPU. Zero-copy into the VDP video plane gave 48.5 fps using 3.8 s: 4× the frame rate at 1/9 of the CPU.

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.


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:



Getting Android 13 up meant a string of silent failures, each fixed in turn:
- Kernel options, among them
EXT4_FS_POSIX_ACL. Without it, vold can't set a default ACL and system_server loops on "rebooting into recovery". - The GPU only does GLES 2.0. SurfaceFlinger crashes unless Mesa's version override is set in the environment (an init
.rcin the vendor overlay; a property doesn't work) and boot-time shader priming is turned off. - Waydroid's networking assumes legacy iptables, which 6.18 gates behind an extra option; its nftables path works.
What doesn't (yet)
Being honest about the gaps:
- SATA is physically marginal. After a reseat the link trained only at 1.5 Gbps. Under load I saw 20 hard resets, a drop to UDMA/33, and the CRC error count climbed from 17 to 26 at ~0.2 MB/s. The disk itself is healthy by SMART. The decisive test was the factory Android: it couldn't see the disk either, with its own register values identical to the ones I programmed. So it's the cable or connector, not software. A new cable is on order, and the box runs from a USB stick until then.
- Android has no internet. My kernel lacks policy routing (
CONFIG_IP_ADVANCED_ROUTER), and Android's netd needs it to install a default route. The box side (bridge, NAT) is fine. It's a one-line kernel option, parked until the SATA disk is back. - Android on a USB 2 stick is slow. The first boot took ~50 min (it compiles every app), a restart 7 min. From the SATA disk it was 100 s.
- No Netflix-class DRM. Measured on the stock firmware: Widevine L3 (OEMCrypto v11). L1 never initialises. The stock build is an
engbuild named after the SoC, not a certified device, so as far as I can tell Netflix HD was never on the table, even in 2016. - 32-bit only. The boot firmware and TEE are 32-bit, so the kernel is AArch32 and arm64-only apps won't install.
- Stock Android 7 via kexec boots but the TV shows stripe noise. It's opt-in only; pulling the stick boots it normally, since the eMMC was never written.
Mistakes I made, kept on purpose
These cost real time, so they stay in the notes:
- I claimed things before checking them. "The kernel booted, Ethernet is up" turned out to be an IP camera answering on the address I'd chosen. Five build-and-boot rounds rested on it. One
arplookup would have caught it. Now every claim gets tagged read, inferred or unknown. - Mainline
reboothangs on this SoC. It has no restart handler, and my boot unit had already disarmed the watchdog, so the box sat at "Restarting system" until someone pulled the plug. That also confounded an earlier conclusion ("images written on the box don't boot"), which I had to retract. - A watchdog timeout that overflows the 32-bit register (300 s × 24 MHz) arms some other window. The guard scripts now refuse anything outside 1–178 s.
- A verifier that only knows one encoding reported present code as missing: GCC built a constant as a rotated immediate, and PID 1 turned out to be Thumb-2 while the kernel is ARM.
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.