Files
MontaukOS/kernel/src/Drivers/Platform/RpiFirmware.cpp
T

276 lines
11 KiB
C++

/*
* RpiFirmware.cpp
* Raspberry Pi VideoCore firmware mailbox (property interface)
* Copyright (c) 2026 Daniel Hammer
*/
#include "RpiFirmware.hpp"
#include <ACPI/AcpiDevices.hpp>
#include <Arch/Cpu.hpp>
#include <CppLib/Spinlock.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
#include <Boot/Boot.hpp>
#include <Memory/Memmap.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Terminal/Terminal.hpp>
#include <Timekeeping/Timer.hpp>
// 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;
}
}