Flash 168 lines
// 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
}