meta-az0x scarthgap-v1.0: Yocto Linux on Denon DJ hardware

Today I’m releasing meta-az0x, a Yocto Project board support layer for inMusic’s AZ0x platform: the Rockchip-based hardware underneath Denon DJ, Akai MPC, Numark and Rane gear.

With it, you can build your own Linux for a Denon DJ SC6000, Prime Go, or other Denon DJ devices with a mainline-based kernel built from source, flash it over fastboot, and the device’s own bootloader boots it. No EngineOS, no vendor application, and nothing outside the root filesystem partition changes, so going back to stock is a couple fastboot commands.

A Denon DJ SC6000 running Mixxx on a Yocto-built Linux, with album art on the jog-wheel display

That’s an SC6000 running Mixxx on a stock Poky image, with album art on the jog wheel.

The background research (firmware layout, UART access, device trees, Engine internals) lives on my documentation site: https://deathcamel58.github.io/denon-reverse-engineering/.

What you get

SC6000 (az01-premium)Prime Go (az01-player)
Touchscreen displayyesyes
GPU (Panfrost, OpenGL ES 3.1)yesyes
Audio, Wi-Fi, Bluetoothyesyes
Powerreboot, reboot to fastboot, power offDC jack and battery reporting
Extrasall USB ports, the internal SATA drive bay, the control surface as a MIDI device, the jog-wheel display

The layer itself provides:

  • A kernel built from source: linux-yocto 6.6, optionally PREEMPT_RT, with device trees for every AZ01/AZ05 board, plus drivers for the DC-jack power supply, the DSI panels, the audio codec and machine driver, and the internal USB hub.
  • Machine configurations for the AZ01 family (RK3288), and a scaffold for AZ04 (RK3588).
  • Board support: panel rotation, touch, the boot counter, network defaults and the jog-wheel display.
  • Two reference images: a minimal console image, and a Sato touch desktop rotated for the panel, with an on-screen keyboard.

It’s a BSP layer and nothing more. Bring Poky or your own distro, and put whatever applications you like on top.

Everything is MIT, except the kernel patches, which are GPL-2.0-only like the kernel. The vendor firmware the hardware needs (board-tuned Broadcom files, touch and DSP blobs) isn’t redistributed. You extract it from your own device’s firmware update (see Extracting).

Getting started

mkdir az0x && cd az0x
git clone -b scarthgap https://git.yoctoproject.org/poky
git clone -b scarthgap https://git.openembedded.org/meta-openembedded
git clone -b scarthgap https://git.yoctoproject.org/meta-arm
git clone -b scarthgap https://git.yoctoproject.org/meta-rockchip
git clone -b scarthgap-v1.0 https://github.com/DeathCamel58/meta-az0x

# stage vendor/<machine>/ from your device's firmware update, then:
TEMPLATECONF=$PWD/meta-az0x/conf/templates/default source poky/oe-init-build-env build
bitbake az0x-image-desktop                       # SC6000
MACHINE=az01-player bitbake az0x-image-desktop   # Prime Go

Put the device in fastboot mode: run reboot loader from a running system, or on a Prime Go hold < Load and Load > while powering on. Then flash:

fastboot oem inmusic-unlock-magic-7de5fbc22b8c524e
fastboot flash rootfs tmp/deploy/images/az01-premium/az0x-image-desktop-az01-premium.rootfs.ext4
fastboot reboot

To go back to stock, flash the rootfs.img from a stock update the same way. Leave the other oem commands alone (format, resetenv, update-bootloader, flash-bootloader); they’re destructive.

On the SC6000

Terminal on the SC6000's own touchscreen
root@az01-premium:~# uname -a
Linux az01-premium 6.6.151-yocto-standard #1 SMP Tue Aug 18 18:34:22 UTC 2026 armv7l GNU/Linux
root@az01-premium:~# cat /proc/device-tree/model; echo
Denon DJ SC6000 Prime
root@az01-premium:~# grep PRETTY_NAME /etc/os-release
PRETTY_NAME="Poky (Yocto Project Reference Distro) 5.0.21 (scarthgap)"
root@az01-premium:~# lscpu | grep -E 'Model name|^CPU\(s\)|max MHz'
CPU(s):                                  4
Model name:                              Cortex-A17
CPU(s) scaling MHz:                      100%
CPU max MHz:                             1608.0000
root@az01-premium:~# free -h | head -2
              total        used        free      shared  buff/cache   available
Mem:        2046908      102352     1804228       24160      140328     1859180
root@az01-premium:~# glxinfo -B | grep -E 'renderer|version string'
OpenGL renderer string: Mali-T760 (Panfrost)
OpenGL core profile version string: 3.1 Mesa 24.0.7
OpenGL version string: 3.1 Mesa 24.0.7
OpenGL ES profile version string: OpenGL ES 3.1 Mesa 24.0.7
root@az01-premium:~# xrandr | head -3
Screen 0: minimum 320 x 200, current 1280 x 800, maximum 4096 x 4096
DSI-1 connected primary 1280x800+0+0 left (normal left inverted right x axis y axis) 0mm x 0mm
   800x1280      60.06*+
root@az01-premium:~# lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 15e4:900d Numark SC6000 Prime Wheel Display
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 002: ID 15e4:e00d Numark SC6000 PRIME Hub
Bus 003 Device 003: ID 152d:0578 JMicron Technology Corp. / JMicron USA Technology Corp. JMS578 SATA 6Gb/s
Bus 003 Device 004: ID 15e4:800d Numark SC6000 Prime Controller
  • Upstream through and through: a plain linux-yocto kernel on stock Poky scarthgap, booted by the SC6000’s own U-Boot.
  • Open-source graphics: the RK3288’s Mali-T760 runs on Panfrost in upstream Mesa, OpenGL ES 3.1, with no vendor GPU blob.
  • A full desktop in about 100 MB of RAM: the Sato desktop, out of 2 GB.
  • A portrait panel mounted sideways: the display is natively 800×1280, and X presents it as 1280×800, rotated from a device-tree property.

And the GPU with Mesa’s built-in HUD over glmark2, holding about 50 fps fullscreen at about 20% CPU:

glmark2 on the SC6000 with Mesa's FPS and CPU overlay
GALLIUM_HUD=fps,cpu glmark2-es2 --fullscreen -b shading:duration=60

The SC6000 is a strange machine

Everything inside it is a USB device

Look at that lsusb again. The jog-wheel display, the control surface (every button, knob and fader on the deck) and the SATA drive bay are all USB devices. 15e4 is inMusic’s vendor ID, which usb.ids still files under Numark.

None of the ports, the drive bay or the control surface hang off the SoC directly. They sit behind a Microchip USB hub that powers up with every port disabled and calls itself a generic SMSC USB2137B. It has to be configured over SMBus before it does anything. EngineOS does that with a “virtual mux” driver that applies a device-tree overlay at runtime.

meta-az0x expresses the end state statically instead:

  • two gpio-hogs release the hub’s reset and switch the USB mux to the internal side;
  • a small driver streams the board’s configuration blob, which lives in the device tree, to the hub over SMBus, then sends it USB_ATTACH.

The hub then re-enumerates as SC6000 PRIME Hub, and everything behind it appears. Because the driver is board-agnostic, the same code covers 24 boards’ device trees.

The jog wheel has its own screen, and it speaks MIDI

The SC6000 jog-wheel display showing album art and the platter ring, driven from Mixxx

The round display in the middle of the platter isn’t a framebuffer. It’s a separate USB-MIDI device (15e4:900d) with its own graphics stored on board:

  • the Engine logo, the platter ring and its position indicator;
  • the slip ring and indicator, and the track progress ring;
  • loop-size text, jump and loop icons, and a “burst” effect;
  • two slots for album art.

The host never draws pixels. It switches elements on and off, sets their colors, and moves the platter indicator by sending a 14-bit angle as MIDI pitch bend, 16384 steps per revolution. It’s sent on two channels, which appear to carry the playing position and the slip position.

Album art is the fun part. It’s a 198×198 PNG, rotated 180° and masked to a circle, then sent over SysEx. SysEx can only carry 7-bit bytes, so the PNG is packed as a continuous MSB-first 7-bit bitstream, in 3500-byte chunks, followed by a 32-bit checksum. The display decodes it on board. I worked out the format from Engine’s own binary in Ghidra, and checked it by re-encoding seven captured uploads to reproduce Engine’s messages byte for byte.

The display also has a watchdog: if no host sends a keepalive, it resets itself about every 10 seconds (Engine sends one every 40 ms). A MIDI device inquiry releases it to its idle logo, and meta-az0x ships a udev rule that does exactly that whenever the display appears. Applications that want to drive it then take over cleanly.

More on the MIDI side is at Engine MIDI Devices.

The touch glass and the LCD disagree about which way is up

The SC6000’s panel is portrait, mounted landscape, so the display is rotated 270°. The touch digitizer, though, is mounted to follow the landscape bezel, so it needs 180°: the square of the display’s rotation.

meta-az0x handles that in the device tree, where it belongs: touchscreen-inverted-y plus touchscreen-swapped-x-y, which mainline ili210x applies invert-first. Touch is correct with no calibration step, and the Prime Go’s identical-looking screen needs no such correction.

The “restart” GPIO is really a power latch

The AZ01 boards keep themselves powered with a latch: U-Boot drives a GPIO called PWR_HOLD high at boot to hold the supply on. The Rockchip global reset that restarts the SoC also releases that latch, so a naive reboot is also a power-off.

The vendor’s answer is a gpio-restart node on a neighboring line, PWR_OFF_DELAY. It fires first on reboot and holds the supply up across the reset, until U-Boot latches PWR_HOLD again. It’s a power hold wearing a restart handler’s clothes.

Mainline gpio-restart assumes its GPIO resets the board, and warns when the board is still running afterwards. meta-az0x teaches it the difference with a new power-hold device-tree property, so AZ01 reboots cleanly while every other board keeps the warning.

Fastboot is a magic number

Fastboot mode is just a value in a PMU register that survives a reset: 0x5242c301 means “loader”. The kernel writes it for reboot loader, and you can do the same by hand at the U-Boot prompt:

=> mw 0xff730094 0x5242c301
=> reset

The same PMU block holds a boot counter that U-Boot bumps on every boot. Enough warm reboots in a row without anything clearing it, and the bootloader decides the OS is failing and drops to fastboot. meta-az0x clears it after every successful boot.

The RK3288 has a USB controller you must never touch

The RK3288’s OHCI controller at 0xff520000 is broken silicon; Rockchip fixed it in the RK3288W. On these boards, once it idles, a single register read freezes the whole SoC. The vendor U-Boot leaves the node enabled, so meta-az0x’s kernel ships with no driver for it. The EHCI half of the same port works fine and handles everything through the hub.

The Prime Go

Its display setup lives in the bootloader

The Prime Go’s panel is an ILI9881C-class controller, now driven by mainline’s panel-ilitek-ili9881c with the inMusic panels added. Its 194-command initialization sequence came out of the vendor U-Boot, a Thumb-2 binary read straight off the eMMC. Several interchangeable panels from different suppliers share the same mode, so the factory U-Boot can autodetect between them.

It knows it’s plugged in by measuring the battery

The Prime Go runs on a 4S Li-ion pack, and decides whether it’s on mains power with an ADC channel on the DC jack, behind a 16:1 divider. Unplugged, that channel reads the battery’s own terminal voltage, up to 16.8 V on a full pack. The adapter sits at about 18.6 V. So the threshold has to fall in the gap between a full battery and the adapter. meta-az0x’s DC-jack driver uses 17.5 V and states it explicitly in the device tree.

The battery itself is a standard Smart Battery (SBS) pack on I²C, so mainline sbs-battery reports charge, health and cycle count with no extra code.

One audio driver, dozens of products

Every AZ0x product shares one codec driver and one machine driver. What differs per product (sample rates, channel counts, reset and unmute timing) lives in a table keyed by the device-tree compatible. I extracted that table from the vendor’s kernel modules; it’s byte-identical across two kernel versions built 16 months apart. The sample rate is selected with two GPIOs encoding 44.1, 48, 88.2 or 96 kHz, and each product enables the rates its hardware supports. The Prime Go, for example, runs at 44.1 kHz only.

Swap the kernel in one line

AZ0X_KERNEL_PROVIDER picks who supplies the kernel:

  • yocto: linux-yocto 6.6 with this layer’s patches (the default on the SC6000, Prime Go and console boards);
  • yocto-rt: the same with PREEMPT_RT;
  • prebuilt: the vendor’s own kernel, device trees and modules, from your firmware dump.

Your own userspace boots on either kernel, which makes it easy to compare the two on the same hardware.

What’s next

  • Applications: Mixxx 2.6 already runs on the SC6000 itself. The control surface is mapped, and the jog-wheel display shows album art, the platter position and loop state. That lives in a separate application layer I’ll release later. meta-az0x stays a BSP, so you can put anything on top.
Mixxx 2.6 playing a track on the SC6000's own screen
  • Persistent storage: the root filesystem is read-only today. The device’s unused data partition is the obvious home for settings.
  • More boards: device trees exist for every AZ01/AZ05 board I could find. If you have other hardware on this platform and a UART adapter (UART notes), I’d love test reports.

Issues and pull requests go to https://github.com/DeathCamel58/meta-az0x. For hardware details, see the device trees, displays and the rest of the documentation site.

Not affiliated with inMusic Brands or Denon DJ. Flashing your own root filesystem is at your own risk. Keep a stock rootfs.img around.