// BCM2711 VideoCore mailbox — MMIO doorbell for the property channel. // // Pairs with the pure src/mailbox.zig (message layout + parsing). The // EMMC2 driver uses it twice during bring-up: to read the firmware- // set EMMC2 base clock, and to drop the SD card's 1.8 V supply so the // card re-inits at 3.3 V. // // The property buffer lives in .bss. The kernel runs with the data // cache off and all RAM mapped Normal-Non-Cacheable (see MAIR_EL1 / // SCTLR in arch/aarch64/asm_defs_common.inc), so ARM writes hit RAM directly // and the VideoCore sees them without any cache maintenance. The // buffer sits in the low <16 MiB identity-mapped window, so its // virtual address equals its physical address — exactly what the // doorbell wants. const mailbox = #import("mailbox") const LINEAR_MAP_BASE u64 = 0xFFFF000000000000 const DEVICE_BASE u64 = 0xFE000000 const MBOX_BASE u64 = DEVICE_BASE + 0xB880 + LINEAR_MAP_BASE const MboxRegs = extern struct { read u32, // 0x00 _reserved [3]u32, // 0x04..0x0F peek u32, // 0x10 sender u32, // 0x14 status u32, // 0x18 config u32, // 0x1C write u32 // 0x20 } inline fn regs() *mut volatile MboxRegs { return #ptrFromInt(MBOX_BASE) } // Full system data barrier. Orders the ARM's plain writes/reads of the // `prop_buf` (Normal-Non-Cacheable RAM) against the volatile doorbell // register traffic, so the request words are published before the // doorbell rings and the response words are read only after the // completion doorbell is observed. The "memory" clobber also stops the // compiler reordering the buffer accesses across the barrier. inline fn dsb() void { asm volatile ("dsb sy" : : : .{ .memory = true }) } const STATUS_FULL u32 = 0x8000_0000 const STATUS_EMPTY u32 = 0x4000_0000 // Property-tag message buffer. 16-byte aligned so the doorbell's low // nibble is free for the channel id. var prop_buf mailbox.Msg align(16) = undefined // Generous spin bound — a property call answers in microseconds on // real hardware; the bound only exists so a wedged VideoCore turns // into a clean failure instead of a hang. const SPIN u32 = 1_000_000 // Post `prop_buf` on the property channel and wait for the matching // response. Returns false on a spin-bound timeout. Callers inspect // the buffer afterwards for the per-tag result. fn transact() bool { const r = regs() // .bss in the id-mapped low window → VA == PA, fits in u32. const msg u32 = mailbox.doorbell(#intCast(#intFromPtr(&prop_buf)), mailbox.CHANNEL_PROP) // Drain any stale response a prior transaction left in the read // FIFO. The doorbell word is identical for every property call // (fixed buffer address + fixed channel), so a leftover entry would // satisfy the `r.read == msg` match below and be taken for THIS // call's response — returning before the VideoCore has refreshed // `prop_buf`, so the parse reads a stale/half-written buffer and the // call degrades to "unknown". This is the back-to-back race: the // first property read per command drains clean and succeeds, the // second matches the first's leftover. Flush first so the match can // only catch the fresh reply. var drain u32 = 0 while ((r.status & STATUS_EMPTY) == 0) { _ = r.read if (drain >= SPIN) { break } drain += 1 } // Publish the request words before ringing the doorbell. dsb() var spin u32 = 0 while ((r.status & STATUS_FULL) != 0) { if (spin >= SPIN) { return false } spin += 1 } r.write = msg spin = 0 while true { if (spin >= SPIN) { return false } if ((r.status & STATUS_EMPTY) == 0) { if (r.read == msg) { break } } spin += 1 } // Order the buffer reads after observing the completion doorbell. dsb() return true } // Query a VideoCore clock rate in Hz. Returns 0 on any failure // (mailbox wedged, bad response) — callers treat 0 as "unknown" and // degrade gracefully. pub fn getClockRate(clock_id u32) u32 { mailbox.buildGetClockRate(&prop_buf, clock_id) if (!transact()) { return 0 } return mailbox.parseClockRate(&prop_buf, clock_id) catch 0 } // Read the SoC temperature in milli-degrees Celsius. Returns 0 on any // failure (mailbox wedged, bad response) — callers treat 0 as "unknown". pub fn getTemperature() u32 { mailbox.buildGetTemperature(&prop_buf, 0) if (!transact()) { return 0 } return mailbox.parseTemperature(&prop_buf, 0) catch 0 } // Read the ARM (CPU) core clock in Hz — the firmware-reported rate, not // a fixed constant (it scales with DVFS). Returns 0 on any failure. pub fn getCpuClock() u32 { return getClockRate(mailbox.CLOCK_ID_ARM) } // Set a firmware-managed GPIO (e.g. the Pi 4 expander lines). Returns // false on any failure. pub fn setGpioState(gpio u32, state u32) bool { mailbox.buildSetGpioState(&prop_buf, gpio, state) if (!transact()) { return false } mailbox.checkResponse(&prop_buf) catch return false return true } // Drive a firmware-managed power rail (e.g. the SD-card VDD on Pi 4). // `state` is the bitwise OR of `POWER_STATE_ON`/`OFF` with optionally // `POWER_STATE_WAIT`. Returns false on any mailbox or device failure; // when ON was requested, also returns false if the rail did not come up. pub fn setPowerState(device_id u32, state u32) bool { mailbox.buildSetPowerState(&prop_buf, device_id, state) if (!transact()) { return false } const want_on = (state & mailbox.POWER_STATE_ON) != 0 mailbox.parsePowerState(&prop_buf, device_id, want_on) catch return false return true }