276 lines
11 KiB
C++
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;
|
|
}
|
|
|
|
}
|