Why this thing

The Ruckus H510 is an in-wall 802.11ac AP that shipped into MDU and hospitality installs. It’s built around Qualcomm’s IPQ40xx SoC, has 512 MiB of RAM, 128 MiB of NAND, and a 4 MiB SPI NOR for the boot chain. It’s also end-of-life: Commscope isn’t shipping firmware updates anymore, and the stock Unleashed firmware is stuck on an old-ish kernel with a proprietary userspace (v54_bsp) that most operators have no reason to trust or maintain.

No Ruckus device exists upstream in target/linux/ipq40xx/ as of 2026-04-24, so this was a from-scratch port. Here’s how it went.

Phase 1: Serial recon

First step: get a console on the thing. The H510 has a populated 4-pin header on the board (TX=pin 1, GND=pin 4, RX=pin 5). FT232 at 115200 8N1. The SoC is an IPQ40xx family (Dakota) — the boot log confirms AP-DK04.1-C1 reference design, quad-core Cortex-A7 [410fc075], 512 MiB RAM.

The U-Boot requires Ctrl-C (not any key) to interrupt the 4-second autoboot countdown. Timing a single keypress is a coin flip. The technique that worked: spam Ctrl-C bytes (0x03) in a tight loop for the whole PBL → SBL1 → U-Boot window (~15–25 seconds):

for i in $(seq 1 500); do
    printf '\x03' > /dev/ttyUSB0 2>/dev/null
    sleep 0.05
done

With a background cat /dev/ttyUSB0 > capture.log & reading serial, this is fully automatable. The excess Ctrl-Cs just print <INTERRUPT> harmlessly after the prompt appears.

A single boot log gives you almost everything: SoC family, RAM size, flash layout, PHY IDs, reserved memory regions, kernel version, and the board identifier strings. For locked-bootloader devices this is the cheapest, safest source of truth.

Phase 2: U-Boot recon (reads only)

Never nand write, never sf erase, never saveenv until you’ve dumped everything read-only. Key finds from the U-Boot session:

The stock DTB is the single most valuable artifact. On any OpenWrt-kernel-based OEM, the DTB is embedded in a FIT image inside a UBI volume. Ruckus wraps the FIT with a 160-byte rcks_wlan header, but U-Boot can crack it:

setenv mtdids nand0=nand0
setenv mtdparts mtdparts=nand0:48m(main),48m(bkup),32m(data)
ubi part bkup
ubi read 0x84100000 kernel
imxtract 0x841000a0 fdt@1 0x84500000
fdt addr 0x84500000
fdt print /soc

This gave us 8 LED GPIOs, the reset button, the TPM/I2C reset lines, and the switch port ↔ MDIO mapping in a single session.

Full SPI NOR backup via TFTP put — nuclear recovery fallback:

sf read  0x86000000 0 0x400000
tftpput 0x86000000 0x400000 h510-spinor-backup.bin

This covers SBL1, MIBIB, QSEE, CDT, DDRPARAMS, APPSBL (U-Boot), ART, Board Data — everything short of NAND contents.

Phase 3: DTS drafting

Every non-trivial claim in the DTS carries an inline comment citing the capture file and line where it was observed:

memory {
    device_type = "memory";
    /* 512 MiB confirmed: attempt-2-uboot.log "DRAM: 512 MiB" */
    reg = <0x80000000 0x20000000>;
};

The nearest upstream reference for the IPQ4019 Dakota: Aruba Instant On AP-303H (qcom-ipq4029-ap-303h.dts). It’s the closest architectural match — enterprise AP, dual integrated radios, TPM, NAND + SPI NOR. Meraki MR33 as a cross-check for locked-bootloader workarounds on this SoC.

Dead ends

“The R500 is the template” — wrong. R500 is ath79 / QCA9557 (MIPS, 64 MiB SPI NOR, 256 MiB RAM). Completely different architecture. The mistake was caught in the first 30 seconds of the boot log — Format: Log Type - Time(microsec)... is a Qualcomm SBL1 signature, not Atheros.

“The R510 is upstream in ipq40xx” — also wrong. Verified via git ls-files in the OpenWrt tree: zero *ruckus* DTS files anywhere under target/linux/ipq40xx/dts/ as of 2026-04-24.

Lesson: grep the tree before claiming a device is upstream. Even for devices that “feel like they should be” supported.

Phase 4: Build + TFTP-boot loop

All build tooling runs inside a Debian bookworm Docker container matched to the host UID/GID. The WSL2 host stays clean, the build is reproducible, and subsequent DTS-only rebuilds after the first toolchain bootstrap are 2–5 minutes.

The initramfs FIT image (h510-initramfs-fit-uImage.itb) boots via:

tftpboot 0x84000000 h510-initramfs.itb
bootm 0x84000000

First boot revealed:

  • GPIO polarity was wrong. The stock Ruckus DTB declares all 8 LED GPIOs as GPIO_ACTIVE_LOW. The H510’s actual wiring is active-high — physical HIGH lights the LED. Ruckus’s proprietary v54_bsp driver silently inverts; the upstream gpio-leds driver doesn’t. Symptom: every LED was lit because “off” (deasserted active-low = physical HIGH) was actually the on state. Diagnosed by toggling and observing, then flipping every flag to GPIO_ACTIVE_HIGH.

  • MAC offset off-by-one. First DTS draft put the WAN MAC nvmem cell at offset 0x807f. Correct offset is 0x807e. The failure mode was safe — reading 6 bytes from 0x807f gave a multicast bit-set address, which the ethernet driver correctly rejected and fell back to random MAC. Caught because the interfaces came up with 82:54:76:... instead of the sticker 30:87:d9:1c:cd:40.

  • DEVICE_DTS default derivation surprise. The ipq40xx image Makefile derives DEVICE_DTS from the device name via the last underscore-part. For ruckus_h510 that produced h510, but our file was named qcom-ipq4019-ruckus-h510.dts. Fix: explicitly set DEVICE_DTS := qcom-ipq4019-ruckus-h510 in the device stanza.

Phase 5: NAND install + persistence

With the initramfs boot confirmed stable, the next step was flashing OpenWrt into NAND and making it autoboot from cold power-up.

The H510 uses an A/B boot scheme: rcks_wlan.main (slot A, 0x0–0x3000000) and rcks_wlan.bkup (slot B, 0x3000000–0x6000000) with a UBIFS datafs in the remaining 32 MiB. We flash OpenWrt into slot B and pin bootcmd to always boot it.

From U-Boot:

tftpboot 0x84000000 h510-factory.ubi
nand erase 0x3000000 0x3000000
nand write 0x84000000 0x3000000 <size>

The bootcmd environment needs two paths: one for slot A (stock Ruckus, using bootlcl rcks_wlan.main — the Ruckus U-Boot’s purpose-built command that handles TZ handoff) and one for slot B (OpenWrt, using bootm). The naive bootm 0x840000a0 for slot A crashes with external abort on non-linefetch because it skips the TZ handoff that the stock kernel requires.

From the running OpenWrt system, fw_setenv sets the U-Boot environment to always boot slot B. The AP now autoboots OpenWrt on power-up. Slot A is untouched as a one-command fallback.

A notable gotcha: the 0:CDT (Configuration Data Table) partition is exposed as a writable /dev/mtdN from the OEM kernel with no write protections. A typo on the mtd number and the device is hard-bricked before U-Boot even runs — SBL1 validates CDT by magic

  • length, and a corrupted CDT means no bootloader handoff. Recovered on one unit via CH341A + SOIC-8 clip with the SPI NOR backup.

Rule going forward: never dd/cat/echo to any /dev/mtdN whose partition has a 0: prefix except 0:APPSBLENV. Use mtd_debug erase + mtd_debug write if you must, never the raw block device.

Phase 6: WiFi

Both ath10k radios (2.4 GHz and 5 GHz, integrated into the IPQ4019) come up, but the stock Board Data in the SPI NOR 0:ART partition returns zeros via BMI (bmi-board-id). Ruckus’s stock v54_bsp driver writes calibration via a side channel that the upstream ath10k driver doesn’t replicate.

The workaround: custom board file generated via ath10k-bdencoder, using the upstream bmi-board-id=16/17 reference-design entries from the IPQ40xx DK04.1 reference board as payload. Each radio gets a per-radio variant string (Ruckus-H510-2g / Ruckus-H510-5g) to distinguish them in the board file lookup.

This is now packaged properly as ipq-wifi-ruckus_h510: package/firmware/ipq-wifi/files/board-ruckus_h510.QCA4019 holds the board file, and the Makefile calls generate-ipq-wifi-package for it. No more files/ overlay hack — it’s a proper OpenWrt package ready for upstreaming.

What works

  • ✅ Boot from NAND (slot B), autoboot on power-up
  • ✅ Ethernet: 4-port switch (IPQ40xx ESS + QCA8075 PHY), WAN port
  • ✅ Both ath10k radios (2.4 + 5 GHz) with custom board-2.bin
  • ✅ LEDs (power red/green, wifi, director) with correct polarity
  • ✅ Reset button
  • ✅ TPM (Infineon SLB9645 I2C, tpm_tis driver)
  • ✅ Slot A fallback: stock Ruckus firmware untouched in rcks_wlan.main
  • ✅ MAC addresses from proprietary Board Data SPI partition via nvmem

What’s next

The immediate work is upstreaming: DTS cleanup and the 02_network / uboot-envtools additions. The ipq-wifi-ruckus_h510 package is already in place. The H510 is EOL but the same librkscli.so pattern and IPQ40xx reference design show up across the Ruckus Unleashed 200.x line — the R510, R610, and likely others are candidates for the same porting treatment.

The DTS, image recipe, and the ipq-wifi-ruckus_h510 board-file package live on the ruckus-h510 branch of my OpenWrt fork (target/linux/ipq40xx/dts/qcom-ipq4019-ruckus-h510.dts and the matching image stanza).

Lessons learned

Ctrl-C flood is portable. The Ruckus U-Boot needs Ctrl-C specifically, not any-key. The spam technique should work on any Ruckus AP with a similar boot chain.

Template from architecture, not model number. Ruckus re-uses the “R/M/H/T + 3-digit” naming across totally different SoC families. The R500 is MIPS, the H510 is ARM — model-number similarity means nothing.

Vendor DTBs lie about polarity. They’re authoritative for pin numbers but not necessarily for polarity flags or labels. Always toggle and observe on real hardware.

bootlcl vs bootm matters on OEM U-Boots with TZ handoff. The naive bootm approach skips the TZ setup that the stock kernel expects. Use the OEM’s purpose-built boot command for slot A.

MTD_POWERUP_LOCK doesn’t mean locked. The stock SPI NOR driver doesn’t implement the lock/unlock ioctl despite reporting the flag. flash_erase and dd work directly — convenient for install, dangerous for accidental writes.