The Gamestick That Lied Twice: Upgrading a Mislabeled Chinese Emulation Device with EmuELEC 4.8
The Purchase
// Built with
Most of the heavy lifting in this investigation — kernel log analysis, DTB decompilation, device tree patching, SoC identification, and this writeup — was done with Claude (Anthropic's AI), running as an autonomous agent via Claude Code. The human's job was to hold the knife and do the soldering. Not a bad division of labour.
❯ claude.ai | Claude Code
It started with a Chinese gamestick marketed as a retro emulation powerhouse running on an Amlogic S905X3 processor. A popular form factor — compact, plug-and-play, hundreds of games out of the box. The kind of device that floods AliExpress and Amazon at suspiciously low prices.
The first unit purchased turned out to be an obvious scam: an older first-generation S905 chip masquerading as something newer, locked to an ancient version of EmuELEC with no upgrade path. ($30 USD down the drain. The supplier nor Aliexpress gave a single f***, but it will make for a nice gift to a friend). A second purchase from a different more reputed seller seemed to fix that — it came with EmuELEC 4.3, the correct UI, and the spec sheet proudly claiming an S905X3 (Same as the first one BTW).
That's where the real investigation began.
The Upgrade Attempt
Upgrading EmuELEC from 4.3 to 4.8 (the latest official release) using the .tar update method seemed straightforward. It wasn't. The system failed to boot properly, throwing cryptic errors about configuration files not being set up. What followed was a deep dive into the device's internals.
With no SSH access and a Windows PC as the only tool, the investigation required mounting the device's microSD card partitions from Windows. The right approach was using WSL2 with usbipd-win to pass the SD card reader through to a Linux environment and mount the ext4 partition natively — Windows has no reliable native ext4 write support, and WSL2 handles it cleanly without any additional drivers.
The root cause of the boot failure turned out to be a truncated es_systems.cfg — the XML configuration file that tells EmulationStation which game systems exist had been cut off mid-line, corrupted since at least February 2023. The fix was copying the complete version from the EmuELEC SYSTEM squashfs image embedded on the boot partition. The device booted.
The Invisible Lock: Hexadecimal Card Fingerprinting
Before the investigation could move forward, another layer of obstruction revealed itself. The boot scripts on these devices — specifically the custom_start.sh aml_autoscript and bootloader configuration — include logic that on first run checks the hexadecimal identifier of the specific microSD card that ships with the unit. This identifier is effectively a hardware fingerprint unique to that individual card.
The practical consequence: if you attempt to clone the original microSD to a faster, higher quality replacement — a completely reasonable thing to do given that the provided card is invariably a slow, no-name unit that bottlenecks the entire system — the clone will not boot. The bootloader checks the card's identifier, finds it doesn't match what it expects, and refuses to proceed. No error message. No explanation. Just a black screen.
oemid=$(cat /sys/devices/platform/ffe05000.sd2/mmc_host/sd/sd\:0001/oemid)
if [ "$oemid" = "0x6fa2" ] || [ "$oemid" = "0x8fa2" ]; then
touch /storage//test.1
else
touch /storage/test.2
while true; do
systemctl restart emulationstation
sleep 5
done
fi
Nasty Stuff.
The behaviour strongly suggests a deliberate card-binding mechanism, one that serves two practical purposes: it makes the device appear non-functional if modified, discouraging further investigation, and it ensures that the slow storage performance is a permanent feature rather than something the buyer can easily improve.
The workaround was using EmuELEC's own update mechanism, which reflashes the SYSTEM image independently of the original Android boot environment and does not carry this card-binding check. Once EmuELEC is properly installed via the .tar update method, the device boots from any compatible microSD card without restriction.
The Lie Revealed
With SSH now working, a proper audit of the hardware began. The system information screen in EmuELEC 4.3 displayed "Amlogic S905X3" — but three independent checks told a different story.
First, the device tree compatible string:
compatible: amlogic, g12a
Second, /proc/cpuinfo, which on Amlogic hardware reflects the kernel's view of the SoC family, showed G12A lineage — consistent with an S905X2, not SM1/S905X3 silicon. Third, dmesg captured the Mali GPU driver initialising its operating performance point table at hardware-negotiated clock levels consistent with the Mali-G31 found in G12A, not the Mali-G52 paired with the S905X3.
G12A is the internal Amlogic codename for the S905X2 — a different and slightly older chip than the S905X3, which carries the SM1 codename. The device tree, the kernel's CPU identification, and the GPU driver all pointed to the same conclusion independently of each other.
The source of the "S905X3" label was tracked down to a single line in /usr/bin/batocera/batocera-info:
V_CPUMODEL1="Amlogic S905X3"
A hardcoded string. Worth noting: batocera-info is a script from Batocera Linux, not stock EmuELEC. Its presence in this image, carrying a specific model string for a device the official project has no record of, is consistent with a supplier-modified firmware — the image shipped on the device appears to be a custom build rather than a standard EmuELEC release, with the CPU label set at the supplier level to match the marketing claim on the box.
To further test the identification, an actual S905X3 device tree blob was loaded onto the device. The result was an immediate black screen — the hardware could not initialise with SM1 silicon configurations. The result is consistent with G12A hardware that cannot satisfy SM1 initialisation requirements.
The real-time GPU operating frequency can be verified directly via the kernel devfreq subsystem:
cat /sys/class/devfreq/ff9a0000.gpu/cur_freq cat /sys/class/devfreq/ff9a0000.gpu/available_frequencies
These paths expose the hardware's actual negotiated clock table — independent of anything in the device tree or any userspace label.
So to summarise: the first device was a fake first-generation S905 sold as S905X3. The second device was likely an S905X2 sold as S905X3, with the supplier's custom firmware helpfully supplying the false label to back up the marketing claim. As the saying goes — some Chinese sellers are more dishonest than other Chinese sellers.
Fixing What the Update Broke
The 4.8 update had replaced the device tree blob with a generic G12A file that, while broadly correct for the SoC family, had two significant issues:
Yeah, nice themes finally, updated cores!! more performance..., oh wait a minute...
1. GPU frequency scaling was broken. The Mali-G31 GPU had no dynamic frequency scaling exposed to the kernel, meaning it was likely locked out of its upper performance range. Updating to the newer G12A DTB bundled with EmuELEC 4.8's own device tree library fixed this, restoring DVFS with six clock levels from 285MHz up to 846MHz — confirmed via /sys/class/devfreq/ff9a0000.gpu/available_frequencies after the fix. A meaningful improvement for emulation of demanding systems like Dreamcast and PSP.
2. One USB port was dead. The device has two physical USB-A ports. One worked fine. The other would give brief power to anything plugged in, then cut out immediately. After extensive kernel log analysis using the raw /proc/kmsg stream, the most likely cause emerged: the board has two USB controllers — the main xHCI controller managing both physical ports, and a secondary DWC2 OTG controller — both sharing a single USB2 PHY. The OTG controller appeared to be constantly triggering PHY recovery events, causing all connected USB devices to disconnect periodically and making the second port completely unusable.
The fix was disabling the DWC2 OTG controller in the device tree by editing the DTB directly: decompiling the binary blob to a human-readable device tree source with dtc, patching the dwc2_a node's status from "okay" to "disabled", recompiling, and deploying back to the boot partition via SCP. This gave exclusive PHY ownership to the xHCI controller, stabilising both USB ports permanently. The periodic USB disconnect storms that had been firing every 40 seconds ceased entirely.
The Thermal Reality — and the Fix
With the device fully working, thermal performance was measured: 65°C at idle with the original small aluminium heatsink. The heatsink was attached with what appeared to be a factory-applied thermal adhesive pad — functional in theory, but degraded and poorly matched to the die surface in practice.
Removing the heatsink required patience and the right approach. The aluminium was carefully separated from the chip package using a thin knife blade worked slowly around the edges of the adhesive, avoiding any lateral force on the PCB. Residual adhesive on both the heatsink base and the chip package was dissolved and cleaned away with isopropyl alcohol. Any remaining uneven surface on the heatsink base was smoothed with fine sandpaper to ensure flat, consistent contact with the new interface material.
The replacement was a quality thermal pad rated at 6+ W/mK and approximately 1.5mm thick — cut to match the die area only, not the full chip package footprint. The heatsink was pressed firmly on top, compressing the pad slightly to fill microscopic surface irregularities.
The result: idle temperature dropped from 65°C to 44°C — a 21 degree improvement from a careful, low-cost hardware intervention that takes under ten minutes.
The Final State
A device that arrived with a mislabeled processor, a boot-breaking update, a dead USB port, locked storage, broken GPU frequency scaling, and a thermal problem ended up fully restored:
- Running EmuELEC 4.8 stably on any microSD card
- Both USB ports functional and stable
- GPU DVFS working at up to 846MHz
- CPU locked to performance governor at 1.8GHz
- Idle temperature at 44°C
- SoC correctly identified as Amlogic S905X2 (G12A)
- Performance-wise, having updated cores and emulatores does noticeable improvement overall
The Takeaway
These devices are not what they claim to be. The SoC mislabeling pattern — a supplier-modified firmware with a hardcoded CPU string that matches the marketing claim on the box — is consistent with deliberate misrepresentation rather than an innocent configuration mistake. The card fingerprinting behaviour strongly suggests an intentional lock to prevent straightforward storage upgrades. The dead USB port and broken GPU scaling are consistent with a generic firmware applied carelessly to hardware it was never properly matched to.
Buyers should be aware that any Chinese gamestick or TV box claiming an S905X3 deserves scrutiny, futhermore, it is prohibited by Emuelec to distribute and commercialize, and that SSH access to the device tree is the only reliable way to verify what processor is actually inside.
The good news: even a mislabeled, partially broken, deliberately locked device can be completely rescued with the right tools, patience, a willingness to read kernel logs, and a careful hand with a knife and some isopropyl alcohol. Still. DON'T BUY THIS STUFF!
Technical Reference
The commands and patches used throughout this investigation, in order of appearance.
1. Mounting ext4 from Windows via WSL2
Pass the SD card reader from Windows to WSL2 using usbipd-win, then mount the ext4 storage partition natively:
# In Windows PowerShell (admin) — find and attach the SD reader usbipd list usbipd bind --busid <BUSID> usbipd attach --wsl --busid <BUSID> # In WSL2 lsblk # find the device, e.g. /dev/sdb sudo mkdir -p /mnt/sdcard sudo mount -t ext4 /dev/sdb4 /mnt/sdcard # partition 4 = STORAGE ls /mnt/sdcard
2. Identifying the real SoC
Three independent checks, all pointing to G12A (S905X2) rather than SM1 (S905X3):
# 1. Device tree compatible string cat /proc/device-tree/compatible # Output: amlogic,g12a # 2. Kernel CPU identification grep -i hardware /proc/cpuinfo # Output: Hardware : Amlogic # 3. GPU driver OPP table in dmesg (Mali-G31, not Mali-G52) dmesg | grep -i mali | head -20
3. Exposing the hardcoded CPU label
The "S905X3" string shown in the UI came from a single line in the supplier's modified firmware:
# Location of the fake label cat /usr/bin/batocera/batocera-info | grep CPUMODEL # Output: V_CPUMODEL1="Amlogic S905X3" # This string is hardcoded — it reads nothing from hardware.
4. Verifying real GPU frequency via devfreq
Ground-truth GPU clock data direct from the kernel, independent of any device tree or userspace label:
# Current operating frequency (Hz) cat /sys/class/devfreq/ff9a0000.gpu/cur_freq # Full OPP table negotiated with hardware cat /sys/class/devfreq/ff9a0000.gpu/available_frequencies # After DTB fix: 285000000 400000000 500000000 666000000 800000000 846000000 # Governor in use cat /sys/class/devfreq/ff9a0000.gpu/governor
5. Reading raw kernel logs (USB PHY diagnosis)
The standard dmesg buffer wraps quickly on a busy system. Reading /proc/kmsg directly streams continuously:
# Stream kernel log live (Ctrl+C to stop) cat /proc/kmsg # Filter for USB events only cat /proc/kmsg | grep -i usb # The smoking gun — PHY recovery storms from dwc2_a every ~40s: # [ 42.318] dwc2 ff400000.usb: dwc2_handle_usb_port_intr: disconnect # [ 42.319] usb usb1-port2: disconnected # [ 82.441] dwc2 ff400000.usb: dwc2_handle_usb_port_intr: disconnect
6. DTB fix — disabling the conflicting DWC2 OTG controller
Decompile the active DTB, patch the dwc2_a node, recompile, deploy:
# Step 1: Extract the active DTB from the boot partition
# (mount boot partition first — /dev/sdb1 on ext2/vfat)
cp /flash/dtbs/amlogic/meson-g12a-x96-max.dtb ~/device.dtb
# Step 2: Decompile binary DTB to human-readable source
dtc -I dtb -O dts -o device.dts device.dtb
# Step 3: Find and patch the dwc2_a node
grep -n "dwc2_a" device.dts
# Edit the node — change status from "okay" to "disabled":
# &dwc2_a {
# status = "disabled"; <-- change from "okay"
# };
nano device.dts
# Step 4: Recompile back to binary
dtc -I dts -O dtb -o device_patched.dtb device.dts
# Step 5: Deploy back via SCP (from host machine)
scp device_patched.dtb root@<device-ip>:/flash/dtbs/amlogic/meson-g12a-x96-max.dtb
# Step 6: Reboot — both USB ports stable, disconnect storms gone
7. Confirming CPU performance governor
# Check current governor per CPU core
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# Set all cores to performance (persists until reboot)
for cpu in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
echo performance | sudo tee $cpu
done
# Verify max frequency (should be 1800000 = 1.8GHz on S905X2)
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq





Comments
Post a Comment