// mailbox: VideoCore property-tag message construction and parsing. // // Three firmware services are needed to bring up EMMC2: // * "set power state" — defensively enable the SD-card VDD rail // (Circle's CardInit calls this before any controller reset on // Pi 4; the Pi 4 boot firmware leaves VDD on but matching Circle // rules out a half-powered state). // * "set GPIO state" — select the 3.3 V SD I/O rail via expander // line 4 (mailbox GPIO 132 / VDD_SD_IO_SEL: 0 = 3.3 V, 1 = 1.8 V). // * "get clock rate" — read the EMMC2 base clock the SDHCI divider // is derived from (the BCM2711 CAP register's base-clock field is // unreliable, so the firmware value is the only sound source). // This module owns the message layout; the board side // (src/board//mailbox.zig) owns the MMIO doorbell. // // Property message layout (all u32, little-endian): // [0] total buffer size in bytes // [1] request/response code (0 on request, 0x80000000 = OK) // [2] tag id // [3] tag value-buffer size in bytes // [4] tag request/response code // [5] value word 0 // [6] value word 1 // [7] end tag (0) const std = #import("std") // Property mailbox channel (ARM -> VC tag interface). pub const CHANNEL_PROP u32 = 8 // Property tags. pub const TAG_GET_CLOCK_RATE u32 = 0x0003_0002 pub const TAG_SET_GPIO_STATE u32 = 0x0003_8041 pub const TAG_SET_POWER_STATE u32 = 0x0002_8001 pub const TAG_GET_TEMPERATURE u32 = 0x0003_0006 // VideoCore clock ids. pub const CLOCK_ID_EMMC2 u32 = 12 pub const CLOCK_ID_ARM u32 = 3 // VideoCore power-state device ids. Standard RPi mailbox table: // 0 = SD card, 1 = UART0, 2 = UART1, 3 = USB HCD. pub const DEVICE_ID_SD_CARD u32 = 0 // The DWC2 USB-OTG core sits behind the USB HCD power domain. The Pi 4 // boot firmware normally leaves it on, but the USB gadget bring-up pokes // it defensively (same posture as the SD-card rail above). pub const DEVICE_ID_USB_HCD u32 = 3 // Power-state bits. Request: bit 0 = on/off, bit 1 = wait-until-stable. // Response: bit 0 = current on/off, bit 1 = NO_DEVICE (firmware can't // see this device). pub const POWER_STATE_OFF u32 = 0 pub const POWER_STATE_ON u32 = 1 pub const POWER_STATE_WAIT u32 = 2 pub const POWER_STATE_NO_DEVICE u32 = 2 // Pi 4 firmware GPIO-expander lines (the RPi mailbox numbers the // expander from 128). Per bcm2711-rpi-4-b.dts gpio-line-names: line 4 // "VDD_SD_IO_SEL" selects the SD I/O rail (1 = 1.8 V, 0 = 3.3 V). pub const EXP_GPIO_BASE u32 = 128 pub const EXP_GPIO_SD_1V8 u32 = EXP_GPIO_BASE + 4 // Request/response codes for word [1] and the per-tag code word. const CODE_REQUEST u32 = 0x0000_0000 const CODE_RESPONSE_OK u32 = 0x8000_0000 // Flash cannot spell an error set inline in a return type, so the // module's single failure mode is named here. const MailboxError = error{MailboxError} // Every message this module builds is exactly 8 words. The caller // places it in 16-byte-aligned memory the VideoCore can read (see // the board side). pub const Msg = [8]u32 // Fill `buf` with a get-clock-rate request for `clock_id`. pub fn buildGetClockRate(buf *mut Msg, clock_id u32) void { buf[0] = #intCast(buf.len * #sizeOf(u32)) // 32 buf[1] = CODE_REQUEST buf[2] = TAG_GET_CLOCK_RATE buf[3] = 8 // value buffer: clock id + rate buf[4] = CODE_REQUEST buf[5] = clock_id buf[6] = 0 buf[7] = 0 // end tag } // Fill `buf` with a set-GPIO-state request. pub fn buildSetGpioState(buf *mut Msg, gpio u32, state u32) void { buf[0] = #intCast(buf.len * #sizeOf(u32)) // 32 buf[1] = CODE_REQUEST buf[2] = TAG_SET_GPIO_STATE buf[3] = 8 // value buffer: gpio number + state buf[4] = CODE_REQUEST buf[5] = gpio buf[6] = state buf[7] = 0 // end tag } // Fill `buf` with a set-power-state request. The state word is the // bitwise OR of `POWER_STATE_ON`/`OFF` with optionally `POWER_STATE_WAIT` // to make the firmware block until the rail is stable. pub fn buildSetPowerState(buf *mut Msg, device_id u32, state u32) void { buf[0] = #intCast(buf.len * #sizeOf(u32)) // 32 buf[1] = CODE_REQUEST buf[2] = TAG_SET_POWER_STATE buf[3] = 8 // value buffer: device id + state buf[4] = CODE_REQUEST buf[5] = device_id buf[6] = state buf[7] = 0 // end tag } // Fill `buf` with a get-temperature request. `temp_id` selects the // sensor; the BCM2711 exposes a single SoC sensor at id 0. pub fn buildGetTemperature(buf *mut Msg, temp_id u32) void { buf[0] = #intCast(buf.len * #sizeOf(u32)) // 32 buf[1] = CODE_REQUEST buf[2] = TAG_GET_TEMPERATURE buf[3] = 8 // value buffer: temp id + temperature buf[4] = CODE_REQUEST buf[5] = temp_id buf[6] = 0 buf[7] = 0 // end tag } // Check the overall response code the VideoCore stamps into word [1]. pub fn checkResponse(buf *Msg) MailboxError!void { if buf[1] != CODE_RESPONSE_OK { return error.MailboxError } } // Parse a completed get-clock-rate response. Returns the rate in Hz. // Fails if the VideoCore did not stamp the success code, echoed a // different clock id than requested, or reported a zero rate. pub fn parseClockRate(buf *Msg, clock_id u32) MailboxError!u32 { try checkResponse(buf) if buf[5] != clock_id { return error.MailboxError } if buf[6] == 0 { return error.MailboxError } return buf[6] } // Parse a completed get-temperature response. Returns the temperature // in milli-degrees Celsius. Fails if the VideoCore did not stamp the // success code, echoed a different sensor id than requested, or reported // a zero reading (structurally implausible for a powered SoC — treated // as a malformed response, the same posture parseClockRate takes on a // zero rate). The board side maps that failure to its "0 = unknown" // degradation. pub fn parseTemperature(buf *Msg, temp_id u32) MailboxError!u32 { try checkResponse(buf) if buf[5] != temp_id { return error.MailboxError } if buf[6] == 0 { return error.MailboxError } return buf[6] } // Parse a completed set-power-state response. Fails if the overall // response code is not OK, the echoed device id differs, the firmware // reports NO_DEVICE, or (when ON was requested) the rail did not come // up. pub fn parsePowerState(buf *Msg, device_id u32, want_on bool) MailboxError!void { try checkResponse(buf) if buf[5] != device_id { return error.MailboxError } if (buf[6] & POWER_STATE_NO_DEVICE) != 0 { return error.MailboxError } if want_on && (buf[6] & POWER_STATE_ON) == 0 { return error.MailboxError } } // Doorbell word: the property buffer's address with the low nibble // replaced by the channel. The address must already be 16-byte // aligned — the board side guarantees this with `align(16)`. pub fn doorbell(buf_addr u32, channel u32) u32 { return (buf_addr & ~#as(u32, 0xF)) | (channel & 0xF) } // ---- Host tests ---- const testing = std.testing test "buildGetClockRate lays out an 8-word EMMC2 request" { var buf Msg = undefined buildGetClockRate(&buf, CLOCK_ID_EMMC2) try testing.expectEqual(#as(u32, 32), buf[0]) try testing.expectEqual(#as(u32, 0), buf[1]) try testing.expectEqual(TAG_GET_CLOCK_RATE, buf[2]) try testing.expectEqual(#as(u32, 8), buf[3]) try testing.expectEqual(#as(u32, 0), buf[4]) try testing.expectEqual(#as(u32, 12), buf[5]) try testing.expectEqual(#as(u32, 0), buf[6]) try testing.expectEqual(#as(u32, 0), buf[7]) } test "buildSetGpioState lays out an 8-word set-GPIO request" { var buf Msg = undefined buildSetGpioState(&buf, EXP_GPIO_SD_1V8, 0) try testing.expectEqual(#as(u32, 32), buf[0]) try testing.expectEqual(#as(u32, 0), buf[1]) try testing.expectEqual(TAG_SET_GPIO_STATE, buf[2]) try testing.expectEqual(#as(u32, 8), buf[3]) try testing.expectEqual(#as(u32, 0), buf[4]) try testing.expectEqual(#as(u32, 132), buf[5]) try testing.expectEqual(#as(u32, 0), buf[6]) try testing.expectEqual(#as(u32, 0), buf[7]) } test "checkResponse accepts the success code, rejects others" { var buf Msg = undefined buildSetGpioState(&buf, EXP_GPIO_SD_1V8, 0) try testing.expectError(error.MailboxError, checkResponse(&buf)) buf[1] = 0x8000_0000 try checkResponse(&buf) } test "parseClockRate returns the rate on a well-formed response" { var buf Msg = undefined buildGetClockRate(&buf, CLOCK_ID_EMMC2) buf[1] = 0x8000_0000 // VC: overall OK buf[4] = 0x8000_0008 // VC: tag OK + 8 bytes returned buf[6] = 100_000_000 // 100 MHz try testing.expectEqual(#as(u32, 100_000_000), try parseClockRate(&buf, CLOCK_ID_EMMC2)) } test "parseClockRate rejects a missing success code" { var buf Msg = undefined buildGetClockRate(&buf, CLOCK_ID_EMMC2) buf[6] = 100_000_000 try testing.expectError(error.MailboxError, parseClockRate(&buf, CLOCK_ID_EMMC2)) } test "parseClockRate rejects a clock-id mismatch" { var buf Msg = undefined buildGetClockRate(&buf, CLOCK_ID_EMMC2) buf[1] = 0x8000_0000 buf[5] = 1 // VC echoed a different clock buf[6] = 100_000_000 try testing.expectError(error.MailboxError, parseClockRate(&buf, CLOCK_ID_EMMC2)) } test "parseClockRate rejects a zero rate" { var buf Msg = undefined buildGetClockRate(&buf, CLOCK_ID_EMMC2) buf[1] = 0x8000_0000 try testing.expectError(error.MailboxError, parseClockRate(&buf, CLOCK_ID_EMMC2)) } test "doorbell merges the channel into the low nibble" { try testing.expectEqual(#as(u32, 0x0008_1218), doorbell(0x0008_1210, CHANNEL_PROP)) try testing.expectEqual(#as(u32, 0x0010_0008), doorbell(0x0010_000F, CHANNEL_PROP)) } test "buildSetPowerState lays out an 8-word SD_CARD power-on request" { var buf Msg = undefined buildSetPowerState(&buf, DEVICE_ID_SD_CARD, POWER_STATE_ON | POWER_STATE_WAIT) try testing.expectEqual(#as(u32, 32), buf[0]) try testing.expectEqual(#as(u32, 0), buf[1]) try testing.expectEqual(TAG_SET_POWER_STATE, buf[2]) try testing.expectEqual(#as(u32, 8), buf[3]) try testing.expectEqual(#as(u32, 0), buf[4]) try testing.expectEqual(#as(u32, 0), buf[5]) try testing.expectEqual(#as(u32, 3), buf[6]) try testing.expectEqual(#as(u32, 0), buf[7]) } test "buildSetPowerState lays out a USB HCD power-on request" { var buf Msg = undefined buildSetPowerState(&buf, DEVICE_ID_USB_HCD, POWER_STATE_ON | POWER_STATE_WAIT) try testing.expectEqual(#as(u32, 3), buf[5]) // device id = USB HCD try testing.expectEqual(#as(u32, 3), buf[6]) // ON | WAIT } test "parsePowerState accepts a well-formed ON response" { var buf Msg = undefined buildSetPowerState(&buf, DEVICE_ID_SD_CARD, POWER_STATE_ON | POWER_STATE_WAIT) buf[1] = 0x8000_0000 buf[6] = POWER_STATE_ON try parsePowerState(&buf, DEVICE_ID_SD_CARD, true) } test "parsePowerState rejects NO_DEVICE" { var buf Msg = undefined buildSetPowerState(&buf, DEVICE_ID_SD_CARD, POWER_STATE_ON | POWER_STATE_WAIT) buf[1] = 0x8000_0000 buf[6] = POWER_STATE_NO_DEVICE // bit 1 in response = device missing try testing.expectError(error.MailboxError, parsePowerState(&buf, DEVICE_ID_SD_CARD, true)) } test "parsePowerState rejects rail-not-on when ON was requested" { var buf Msg = undefined buildSetPowerState(&buf, DEVICE_ID_SD_CARD, POWER_STATE_ON | POWER_STATE_WAIT) buf[1] = 0x8000_0000 buf[6] = POWER_STATE_OFF try testing.expectError(error.MailboxError, parsePowerState(&buf, DEVICE_ID_SD_CARD, true)) } test "parsePowerState rejects device-id mismatch" { var buf Msg = undefined buildSetPowerState(&buf, DEVICE_ID_SD_CARD, POWER_STATE_ON | POWER_STATE_WAIT) buf[1] = 0x8000_0000 buf[5] = 7 // VC echoed a different device buf[6] = POWER_STATE_ON try testing.expectError(error.MailboxError, parsePowerState(&buf, DEVICE_ID_SD_CARD, true)) } test "buildGetTemperature lays out an 8-word get-temperature request" { var buf Msg = undefined buildGetTemperature(&buf, 0) try testing.expectEqual(#as(u32, 32), buf[0]) try testing.expectEqual(#as(u32, 0), buf[1]) try testing.expectEqual(TAG_GET_TEMPERATURE, buf[2]) try testing.expectEqual(#as(u32, 8), buf[3]) try testing.expectEqual(#as(u32, 0), buf[4]) try testing.expectEqual(#as(u32, 0), buf[5]) try testing.expectEqual(#as(u32, 0), buf[6]) try testing.expectEqual(#as(u32, 0), buf[7]) } test "parseTemperature returns the milli-degrees on a well-formed response" { var buf Msg = undefined buildGetTemperature(&buf, 0) buf[1] = 0x8000_0000 // VC: overall OK buf[4] = 0x8000_0008 // VC: tag OK + 8 bytes returned buf[6] = 47_233 // 47.233 °C try testing.expectEqual(#as(u32, 47_233), try parseTemperature(&buf, 0)) } test "parseTemperature rejects a missing success code" { var buf Msg = undefined buildGetTemperature(&buf, 0) buf[6] = 47_233 try testing.expectError(error.MailboxError, parseTemperature(&buf, 0)) } test "parseTemperature rejects a sensor-id mismatch" { var buf Msg = undefined buildGetTemperature(&buf, 0) buf[1] = 0x8000_0000 buf[5] = 1 // VC echoed a different sensor buf[6] = 47_233 try testing.expectError(error.MailboxError, parseTemperature(&buf, 0)) } test "parseTemperature rejects a zero reading" { var buf Msg = undefined buildGetTemperature(&buf, 0) buf[1] = 0x8000_0000 // VC: overall OK, but temperature word left 0 try testing.expectError(error.MailboxError, parseTemperature(&buf, 0)) }