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.

Two Computers, One Engine: The Part of the ECU I Didn’t Know I Was Deleting

There is a specific kind of bug that only shows up when you remove something. Not when you break it, not when you misconfigure it, but when you take it out entirely and discover that it had a second job nobody wrote down.

I spent a few months this year replacing the engine computer in a 2001 Dodge Stratus with a Speeduino DropBear v2, an open-source standalone ECU. I planned it carefully. I mapped every pin. I wrote a note titled “Confirmed so far.” Then I turned the key and got nothing at all.

The answer was three sentences further down in my own notes, in a warning I had written to myself and then argued my way out of.

Continue reading Two Computers, One Engine: The Part of the ECU I Didn’t Know I Was Deleting

Five Taps and a Zip File: Running My Own Code on a Denon DJ Player

A Denon Prime GO is a $1,000 standalone DJ controller. No laptop, no phone; you put music on a USB stick, plug it in, and play. It has a touchscreen, two decks, a battery, and absolutely no reason to let you run your own software on it.

It does anyway. You tap the version number five times, put a zip file on a flash drive, and it runs whatever you gave it.

I’ve been documenting inMusic’s firmware for a couple of years now. Denon DJ, Numark, Akai, HeadRush, Rane all share a codebase, so work on one device tends to apply to a dozen. Everything in this post lives in my documentation site. But the way into the device is my favorite thing I’ve found in it, partly because of how it works and partly because of a bug I found in inMusic’s own version of it.

Let me walk you through it.

Continue reading Five Taps and a Zip File: Running My Own Code on a Denon DJ Player

The Backdoor That Printed Itself

We’ve all gotten the message you never want to get. Mine came in at 11:57 in the morning: a WordPress site I help maintain had been flagged as compromised. Someone had gotten in and left themselves a way back.

Here’s the part I didn’t expect. By the time I finished reading the malicious code, I realized it had never actually worked. The attacker built a perfectly functional backdoor and then, through one tiny detail, accidentally disarmed it themselves.

Let me walk you through what happened, because the why is genuinely fun, and the lesson underneath it applies to anyone who runs a website.

Continue reading The Backdoor That Printed Itself

Pipelock: Agent Firewall for AI Coding Tools

The Problem: AI Agents Are Credentialed, Networked, and Trusted

When you run Claude Code, Cursor, or any agentic coding tool, you’re handing it something dangerous: a live environment loaded with secrets. Your ANTHROPIC_API_KEY is sitting there. Your AWS credentials. Your GitHub tokens. Your .env file. The agent can read environment variables, make HTTP requests, call MCP servers, and run shell commands — all on your behalf, all in the same process that holds those credentials.

That’s a completely different threat model than a traditional web app. A web app has a defined interface. You know what it talks to. You can write static firewall rules around it. An agent decides at runtime what to do, what to fetch, and what to send. You can’t fully enumerate the attack surface ahead of time because the whole point of the thing is that it improvises.

Continue reading Pipelock: Agent Firewall for AI Coding Tools

Demystifying: The GIF Format

We’ve all seen GIF files being used. They’re the internet’s go-to for animated imagery. I’ve never actually looked into the specification until recently, and I’ve found that it’s a pretty simple file format. This makes GIF a perfect candidate for what’s becoming a recurring “Demystifying” series here.

In this article, we’re going to dive into how GIF files are encoded, and some interesting quirks of the format.

Continue reading Demystifying: The GIF Format

How to Check Your Linux Version: os-release, uname and lsb_release

Ever wonder what Linux version is running on your machine? I commonly connect to friends’ machines, or to one of my servers, and I have no clue what’s on it. This can make a huge difference, as some distributions use apt, some use dnf or yum, and some use something else entirely. Luckily, there are a few commands that answer this in a second.

In this article, we’re going to cover /etc/os-release, uname, lsb_release, and hostnamectl.

Continue reading How to Check Your Linux Version: os-release, uname and lsb_release

Web Phone Links

Phone numbers, everyone knows what they are. Everyone has tapped or clicked on at least one online, and noticed that it automatically pops open the phone app with the number ready to dial.

There are some interesting features of online phone links, that when used properly can make them much more useful. In this article, we’ll dive into phone number links online.

Continue reading Web Phone Links