Date: 2026-06-15 Repos: ~/dev/qemu (implementation), ~/dev/linux (consumer, branch dfustini/atl-sc-cbqri-dt)
Let the QEMU virt machine boot in device-tree mode and have the kernel's
CBQRI cache capacity-allocation platform driver
(drivers/resctrl/cbqri_capacity.c, CONFIG_RISCV_CBQRI_CAPACITY) probe a
modeled CBQRI capacity controller, so the DT driver path can be tested without
the Atlantis HAPS lab machine.
Scope for this iteration: a single shared L2 cache with one capacity
controller backing it. This mirrors the Atlantis sc_cbqri node exactly and
is the smallest change that exercises the driver end to end.
The platform driver binds the generic riscv,cbqri-capacity-controller
compatible and, on probe, reads:
reg-> the CBQRI capacity-controller register block (base, size).riscv,cbqri-rcid-> RCID count.riscv,cbqri-cache-> phandle to the cache node it governs.
The driver does not read the cache geometry from that node. It resolves the
phandle through the kernel's cacheinfo: it walks online CPUs looking for a
cacheinfo leaf at the cache's level whose fw_token equals the cache
device_node, and from that leaf takes the resctrl domain id (cache_id) and
the shared_cpu_map. This only works if the FDT carries a real cache node
(cache-level, cache-unified, etc.) and every sharing hart points at it via
next-level-cache.
Reference DT (Atlantis, arch/riscv/boot/dts/tenstorrent/atlantis-soc.dtsi):
- 8 harts, each with
next-level-cache = <&l2_cache>. l2_cache:cache-level = <2>,cache-unified,cache-size = <0xc00000>.sc_cbqri:reg = <0x0 0xa21a00c0 0x0 0xf40>,riscv,cbqri-rcid = <16>,riscv,cbqri-cache = <&l2_cache>.
- It models the controllers:
hw/riscv/cbqri_capacity.c,hw/riscv/cbqri_bandwidth.c,include/hw/riscv/cbqri.h. - Controllers are instantiated as dynamic sysbus devices on the platform bus,
for example
-device riscv.cbqri.capacity,...,mmio_base=0x04820000. The device self-maps its MMIO at the absolutemmio_basein realize (sysbus_mmio_map), so the address is deterministic. - Controllers are exposed only through ACPI/RQSC.
gatherCbqriDetails()inhw/riscv/virt-acpi-build.cwalksvs->platform_bus_dev->parent_bus->childrento build the RQSC table. virt.cemits no DT node for the controllers and no cache topology at all:create_fdt_socket_cpus()writes nonext-level-cache, and there is no L2/L3 node anywhere in the generated FDT.
So two pieces are missing on the DT side: a CPU cache hierarchy and the controller node.
Chosen: reuse the existing -device instantiation model and emit the DT by
walking the platform bus, the same source of truth the ACPI path already
trusts. Rejected alternatives: a -M virt,cbqri=on machine flag (diverges
from the -device model the test scripts use, adds a machine property); a
hw/core/sysbus-fdt.c binding (runs in the platform-bus context with no
knowledge of the cache phandle created in virt.c, so it cannot emit
riscv,cbqri-cache cleanly, and the cache hierarchy would still have to come
from virt.c).
virt.c builds the base FDT early in create_fdt(), but finalizes it in
finalize_fdt(), which runs from the machine-done notifier
(virt_machine_done) after all -device instances are realized.
finalize_fdt() calls create_fdt_sockets() -> create_fdt_socket_cpus(),
which builds the CPU nodes, and s->platform_bus_dev is populated by then.
Both the cache wiring and the controller walk happen in this one late stage,
the same point at which gatherCbqriDetails() reads the devices for RQSC.
Location: create_fdt_sockets() / create_fdt_socket_cpus() in
hw/riscv/virt.c.
- Allocate one phandle from the existing
phandlecounter for a shared L2. - Emit one node
/cpus/l2-cache:compatible = "cache"cache-level = <2>cache-unified(empty property)cache-size,cache-sets,cache-block-size(mirror Atlantis: 0xc00000 / 512 / 64)phandle
- In the per-CPU loop, add
next-level-cache = <l2_phandle>to every/cpus/cpu@Nnode.
Result: all harts reference the same cache node, so cacheinfo reports one L2
domain whose shared_cpu_map covers every online CPU. Scoped to
sockets=1; the single shared node is created once and its phandle reused
across the CPU loop.
New helper create_fdt_cbqri(RISCVVirtState *s, uint32_t l2_phandle), called
from finalize_fdt() after create_fdt_sockets().
- Walk
s->platform_bus_dev->parent_bus->children; for each child of typeTYPE_RISCV_CBQRI_CC, read generically (no coupling to the device's private struct):base = object_property_get_uint(obj, "mmio_base", &error_abort)rcid = object_property_get_uint(obj, "max_rcids", &error_abort)size = memory_region_size(sysbus_mmio_get_region(SYS_BUS_DEVICE(child), 0))
- Emit
/soc/cache-controller@<base>:compatible = "riscv,cbqri-capacity-controller"(the generic compatible the driver's of_match table binds; no vendor string needed on QEMU)reg = <base size>(two address cells, two size cells, matching/soc#address-cells/#size-cells = 2)riscv,cbqri-rcid = <rcid>riscv,cbqri-cache = <l2_phandle>
For the single-L2 scope every capacity controller points at the one L2 node.
New script ~/dev/qemu/run-dt-sock-1cc.sh, derived from
run-acpi-sock-1bc.sh:
- Drop the EDK2 pflash blockdevs and the
pflash0=/pflash1=machine options; boot the default OpenSBI plus-kernel ${LX}/arch/riscv/boot/Imageso QEMU's generated FDT is passed straight to the kernel (no-dtb). - Keep the socket serial plumbing,
-smp,-m, rootfs blockdev, and a single-device riscv.cbqri.capacity,...,mmio_base=0x04820000line. - Keep
aia=aplic-imsic.
No kernel changes. The consumer branch dfustini/atl-sc-cbqri-dt already
carries the platform driver and binding; build with
CONFIG_RISCV_CBQRI_CAPACITY=y and DT support.
- Boot the DT script; the kernel log shows the driver probe:
cbqri-capacity: ... registered L2 capacity controller at <base> (cache_id=..., rcid=...). /sys/fs/resctrlmounts and exposes the L2 cache-allocation resource with a single domain whose CPU mask covers all harts.- A
schematawrite round-trips: setting a capacity bitmask is observable in the QEMU device (allocation register state).
- L3 or multiple cache levels, per-cluster cache topology.
- Bandwidth-controller DT emission.
- Multiple distinct caches mapped to distinct controllers. The platform-bus walk extends to these naturally later, but they are not built now.
- ~/dev/qemu/hw/riscv/virt.c: cache hierarchy (L1 + shared L2) + next-level-cache;
new
create_fdt_cbqri(); call site infinalize_fdt(). - ~/dev/qemu/run-dt-sock-1cc.sh: new DT-mode launcher.
Two things diverged from the initial design and were resolved:
-
Prerequisite build fix. The
b4/riscv-rqscbase did not compile: after a QEMU rebase,device_class_set_props()became a macro that, at -O2, rejects a legacy terminator by evaluating the array's last element, which only works when the array is const. The CBQRI capacity and bandwidth property arrays were stillstatic Property(non-const), leaving theqemu_build_not_reached()path reachable. Fixed by making both arrays const (its own commit), matching every other device. -
L1 cache properties are required, not just the L2 node. RISC-V
populate_cache_leaves()only creates a cache leaf for a node that carries a*-cache-sizeproperty, while the genericof_count_cache_leaves()assumes a split L1 (counts 2 leaves) for a CPU node with no cache properties. With only the L2 node andnext-level-cache, the two disagree,cache_setup_of_node()bails with -ENOENT, no cacheinfo is built, and the controller'sshared_cpu_mapcomes out empty (driver returns -EINVAL). The fix is to emit split L1 i/d cache properties (i/d-cache-size/sets/block-size) on each CPU node alongsidenext-level-cache, as the working Atlantis DT does. This lives in the same commit as the L2 node (the cache hierarchy is one logical change), gated on CBQRI-CC presence.
Confirmed end-to-end on QEMU virt (DT mode, 8 harts): driver logs
registered L2 capacity controller at 0x4820000 (cache_id=0, rcid=64);
cpu0 cacheinfo shows L2 Unified shared 0-7; /sys/fs/resctrl/info/L2
reports num_closids=64, cbm_mask=ffff; schemata shows L2:0=ffff
with cpus_list=0-7; writing L2:0=0x0f reads back L2:0=f.