/* * RpiFirmware.cpp * Raspberry Pi VideoCore firmware mailbox (property interface) * Copyright (c) 2026 Daniel Hammer */ #include "RpiFirmware.hpp" #include #include #include #include #include #include #include #include #include #include #include // Property interface (raspberrypi/firmware wiki, "Mailbox property // interface"): the ARM writes a message buffer's bus address, tagged with // channel 8, to mailbox 1 and reads the same address back from mailbox 0 // once the firmware has written its responses into the buffer. // // u32 size, u32 code (0 = request; 0x80000000 = success), // tags: { u32 id, u32 value buffer size, u32 req/resp (bit 31 + length), // value buffer ... }, u32 0 (end tag) using namespace Kt; namespace Drivers::Platform::RpiFirmware { // Mailbox registers (offsets from the block ACPI describes, 0x7E00B880) static constexpr uint32_t MBOX_READ = 0x00; // mailbox 0: VC -> ARM static constexpr uint32_t MBOX0_STATUS = 0x18; static constexpr uint32_t MBOX_WRITE = 0x20; // mailbox 1: ARM -> VC static constexpr uint32_t MBOX1_STATUS = 0x38; static constexpr uint32_t STATUS_FULL = 1u << 31; static constexpr uint32_t STATUS_EMPTY = 1u << 30; static constexpr uint32_t CHANNEL_PROPERTY = 8; static constexpr uint32_t CODE_REQUEST = 0x00000000; static constexpr uint32_t CODE_SUCCESS = 0x80000000; static constexpr uint32_t TAG_RESPONSE = 0x80000000; static constexpr uint32_t TAG_GET_BOARD_MAC = 0x00010003; static constexpr uint32_t TAG_GET_CLOCK_RATE = 0x00030002; static constexpr uint32_t TAG_GET_GPIO_STATE = 0x00030041; static constexpr uint32_t TAG_SET_GPIO_STATE = 0x00038041; static constexpr uint32_t TAG_GET_GPIO_CONFIG = 0x00030043; static constexpr uint32_t TAG_SET_GPIO_CONFIG = 0x00038043; // The VideoCore sees the first GiB of SDRAM at bus address 0xC0000000 // (the uncached alias), and nothing above it, so the message buffer // must sit below 1 GiB. static constexpr uint64_t VC_BUS_ALIAS = 0xC0000000; static constexpr uint64_t VC_DMA_LIMIT = 0x40000000; static constexpr uint64_t TimeoutMs = 200; static volatile uint8_t* g_mmio = nullptr; static volatile uint32_t* g_buffer = nullptr; // uncached, below 1 GiB static uint64_t g_bufferPhys = 0; static bool g_probed = false; static kcp::Mutex g_lock; static inline uint32_t Read32(uint32_t offset) { return *(volatile uint32_t*)(g_mmio + offset); } static inline void Write32(uint32_t offset, uint32_t value) { *(volatile uint32_t*)(g_mmio + offset) = value; } // A page the VideoCore can reach. The page-frame allocator only manages // the largest run of usable RAM, which on a Pi 4 with the 3 GB limit is // 0x40000000 and up, so it never has one. The usable regions below 1 GiB // are otherwise untouched: take the last page of the highest one. static void* AllocateLowPage(uint64_t& phys) { const auto& map = montauk::boot::Info().memoryMap; Memory::LargestSection pool = Memory::Scan(map); uint64_t best = 0; for (uint64_t i = 0; i < map.count; i++) { const auto& r = map.regions[i]; if (r.kind != montauk::boot::MemoryKind::Usable || r.base == 0) continue; if (r.base == pool.address) continue; // the allocator's uint64_t end = r.base + r.length; if (end > VC_DMA_LIMIT) end = VC_DMA_LIMIT; if (end < r.base + 0x1000) continue; uint64_t page = (end - 0x1000) & ~0xFFFULL; if (page >= r.base && page > best) best = page; } if (best == 0) return nullptr; // Zero it through its cacheable HHDM alias, push the lines out, then // make the alias uncached so neither side needs cache maintenance. void* virt = (void*)Memory::HHDM(best); memset(virt, 0, 0x1000); Arch::InvalidateDataCache(virt, 0x1000); Memory::VMM::g_paging->MapWC(best, (uint64_t)virt); phys = best; return virt; } bool Initialize() { if (g_probed) return g_buffer != nullptr; g_probed = true; Hal::AcpiDevices::DeviceInfo info{}; if (!Hal::AcpiDevices::FindDeviceByHid("BCM2849", info)) return false; Hal::AML::ResourceList resources; uint64_t base = 0, length = 0; if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) || !resources.GetMemory(base, length) || base == 0) { KernelLogStream(WARNING, "RpiFw") << "Mailbox " << info.Path << ": no register block in _CRS"; return false; } uint64_t page = base & ~0xFFFULL; Memory::VMM::g_paging->MapMMIO(page, Memory::HHDM(page)); if (((base + 0x40) & ~0xFFFULL) != page) { Memory::VMM::g_paging->MapMMIO(page + 0x1000, Memory::HHDM(page + 0x1000)); } g_mmio = (volatile uint8_t*)Memory::HHDM(base); uint64_t phys = 0; void* buffer = AllocateLowPage(phys); if (buffer == nullptr) { KernelLogStream(ERROR, "RpiFw") << "No message buffer below 1 GiB"; g_mmio = nullptr; return false; } g_buffer = (volatile uint32_t*)buffer; g_bufferPhys = phys; KernelLogStream(OK, "RpiFw") << "VideoCore mailbox at 0x" << base::hex << base << ", buffer 0x" << phys << base::dec; return true; } bool IsPresent() { return Initialize(); } // Send the message in g_buffer and wait for the firmware's reply. // Caller holds g_lock. static bool Call() { uint32_t message = (uint32_t)((g_bufferPhys | VC_BUS_ALIAS) & ~0xFULL) | CHANNEL_PROPERTY; // Drop any stale replies (a previous call that timed out). for (int i = 0; i < 16 && !(Read32(MBOX0_STATUS) & STATUS_EMPTY); i++) { (void)Read32(MBOX_READ); } uint64_t start = Timekeeping::GetMilliseconds(); while (Read32(MBOX1_STATUS) & STATUS_FULL) { if (Timekeeping::GetMilliseconds() - start > TimeoutMs) return false; Arch::Pause(); } // The buffer is uncached; the fence orders its stores before the // doorbell write the firmware reacts to. Arch::MemoryFence(); Write32(MBOX_WRITE, message); start = Timekeeping::GetMilliseconds(); for (;;) { while (Read32(MBOX0_STATUS) & STATUS_EMPTY) { if (Timekeeping::GetMilliseconds() - start > TimeoutMs) { KernelLogStream(WARNING, "RpiFw") << "Mailbox call timed out"; return false; } Arch::Pause(); } uint32_t reply = Read32(MBOX_READ); if (reply == message) break; } Arch::MemoryFence(); return g_buffer[1] == CODE_SUCCESS; } // One-tag property call. `values` holds valueWords request words and // receives up to valueWords response words. Returns the response length // in bytes, or -1 on failure. static int PropertyCall(uint32_t tag, uint32_t* values, uint32_t valueWords) { if (!Initialize()) return -1; if (valueWords > 16) return -1; g_lock.Acquire(); uint32_t words = 0; g_buffer[words++] = 0; // size, filled in below g_buffer[words++] = CODE_REQUEST; g_buffer[words++] = tag; g_buffer[words++] = valueWords * 4; // value buffer size g_buffer[words++] = 0; // request uint32_t valueStart = words; for (uint32_t i = 0; i < valueWords; i++) g_buffer[words++] = values[i]; g_buffer[words++] = 0; // end tag g_buffer[0] = words * 4; // The firmware does not set the response bit for every tag: the // expander GPIO tags answer through value[0] alone and leave the // status word 0. Linux checks only the message code, so do the same // and take the whole value buffer back when the bit is missing; each // caller validates the values it gets. int result = -1; bool answered = Call(); uint32_t status = g_buffer[valueStart - 1]; if (answered) { uint32_t length = (status & TAG_RESPONSE) ? status & ~TAG_RESPONSE : valueWords * 4; uint32_t copy = (length + 3) / 4; if (copy > valueWords) copy = valueWords; for (uint32_t i = 0; i < copy; i++) values[i] = g_buffer[valueStart + i]; result = (int)length; } uint32_t code = g_buffer[1]; g_lock.Release(); if (result < 0) { KernelLogStream(WARNING, "RpiFw") << "Property tag 0x" << base::hex << (uint64_t)tag << " failed: message code 0x" << (uint64_t)code << base::dec; } return result; } bool GetClockRate(uint32_t clockId, uint32_t& hz) { uint32_t v[2] = { clockId, 0 }; if (PropertyCall(TAG_GET_CLOCK_RATE, v, 2) < 8 || v[0] != clockId) return false; hz = v[1]; return hz != 0; } // The GPIO tags answer in place of the GPIO number with a status word, // 0 for success (as Linux's raspberrypi-exp-gpio driver checks). bool SetGpioState(uint32_t gpio, bool high) { uint32_t v[2] = { gpio, high ? 1u : 0u }; return PropertyCall(TAG_SET_GPIO_STATE, v, 2) >= 4 && v[0] == 0; } // As Linux's rpi_exp_gpio_dir_out: keep the line's polarity, make it an // output driving `high`. A line the firmware still has as an input // refuses SET_GPIO_STATE. bool SetGpioOutput(uint32_t gpio, bool high) { uint32_t polarity = 0; uint32_t cfg[5] = { gpio, 0, 0, 0, 0 }; // gpio, direction, polarity, term_en, term_pull_up if (PropertyCall(TAG_GET_GPIO_CONFIG, cfg, 5) >= 12 && cfg[0] == 0) polarity = cfg[2]; uint32_t v[6] = { gpio, 1 /* output */, polarity, 0, 0, high ? 1u : 0u }; if (PropertyCall(TAG_SET_GPIO_CONFIG, v, 6) < 4 || v[0] != 0) return false; return true; } bool GetGpioState(uint32_t gpio, bool& high) { uint32_t v[2] = { gpio, 0 }; if (PropertyCall(TAG_GET_GPIO_STATE, v, 2) < 8 || v[0] != 0) return false; high = v[1] != 0; return true; } bool GetBoardMacAddress(uint8_t mac[6]) { uint32_t v[2] = {}; if (PropertyCall(TAG_GET_BOARD_MAC, v, 2) < 6) return false; const uint8_t* bytes = (const uint8_t*)v; bool allZero = true; for (int i = 0; i < 6; i++) { mac[i] = bytes[i]; if (bytes[i] != 0) allZero = false; } return !allZero; } }