Newer
Older
minerva / Kernel / Bus / PCI / DeviceTreeHelpers.cpp
@minerva minerva on 13 Jul 12 KB Initial commit
/*
 * Copyright (c) 2023-2025, Sönke Holz <sholz8530@gmail.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <Kernel/Bus/PCI/Access.h>
#include <Kernel/Bus/PCI/Controller/HostController.h>
#include <Kernel/Bus/PCI/DeviceTreeHelpers.h>
#include <Kernel/Library/StdLib.h>
#include <LibDeviceTree/DeviceTree.h>

namespace Kernel::PCI {

// Common properties for PCI host bridge nodes: https://github.com/devicetree-org/dt-schema/blob/main/dtschema/schemas/pci/pci-host-bridge.yaml
// Common properties for PCI bus structure: https://github.com/devicetree-org/dt-schema/blob/main/dtschema/schemas/pci/pci-bus-common.yaml
// PCI Bus Binding to IEEE Std 1275-1994: https://www.devicetree.org/open-firmware/bindings/pci/pci2_1.pdf

static TriState s_linux_pci_domain_property_used = TriState::Unknown;
static u32 s_next_pci_domain_number { 0 };

ErrorOr<Domain> determine_pci_domain_for_devicetree_node(::DeviceTree::Node const& node, StringView node_name)
{
    // PCI Bus Binding to IEEE Std 1275-1994, 3.1.2. Bus-specific Properties for Bus Nodes:
    // ""bus-range" [...] denotes range of bus numbers controlled by this PCI bus."
    Array<u8, 2> bus_range { 0, 255 };
    auto maybe_bus_range = node.get_property("bus-range"sv);
    if (maybe_bus_range.has_value()) {
        if (maybe_bus_range->size() != sizeof(u32) * 2)
            return EINVAL;

        auto provided_bus_range = maybe_bus_range->as<Array<BigEndian<u32>, 2>>();
        if (provided_bus_range[0] > 255 || provided_bus_range[1] > 255 || provided_bus_range[1] < provided_bus_range[0])
            return EINVAL;

        bus_range[0] = provided_bus_range[0];
        bus_range[1] = provided_bus_range[1];
    }

    // https://github.com/devicetree-org/dt-schema/blob/main/dtschema/schemas/pci/pci-host-bridge.yaml:
    // linux,pci-domain:
    // "If present this property assigns a fixed PCI domain number to a host bridge,
    //  otherwise an unstable (across boots) unique number will be assigned.
    //  It is required to either not set this property at all or set it for all
    //  host bridges in the system, otherwise potentially conflicting domain numbers
    //  may be assigned to root buses behind different host bridges.  The domain
    //  number for each host bridge in the system must be unique."
    u32 domain_number = 0;
    auto maybe_domain_number = node.get_property("linux,pci-domain"sv);

    if (s_linux_pci_domain_property_used != TriState::Unknown && static_cast<TriState>(maybe_domain_number.has_value()) != s_linux_pci_domain_property_used) {
        dbgln("PCI: Either all or no PCI host bridge devicetree nodes must have a \"linux,pci-domain\" property");
        return EINVAL;
    }

    if (maybe_domain_number.has_value()) {
        if (maybe_domain_number->size() != sizeof(u32))
            return EINVAL;

        domain_number = maybe_domain_number->as<u32>();
    } else {
        domain_number = s_next_pci_domain_number++;
    }

    s_linux_pci_domain_property_used = static_cast<TriState>(maybe_domain_number.has_value());

    dbgln("PCI: Assigned domain number {} for {}", domain_number, node_name);

    return Domain {
        domain_number,
        bus_range[0],
        bus_range[1],
    };
}

ErrorOr<void> configure_devicetree_host_controller(HostController& host_controller, ::DeviceTree::Node const& node, StringView node_name)
{
    FlatPtr pci_32bit_mmio_base = 0;
    u32 pci_32bit_mmio_size = 0;
    FlatPtr pci_64bit_mmio_base = 0;
    u64 pci_64bit_mmio_size = 0;
    HashMap<PCIInterruptSpecifier, u64> masked_interrupt_mapping;
    PCIInterruptSpecifier interrupt_mask;

    auto const& device_tree = DeviceTree::get();

    auto const* parent = node.parent();

    if (parent == nullptr)
        return EINVAL;

    auto maybe_ranges = node.ranges();
    if (!maybe_ranges.is_error()) {
        dbgln("PCI: Address mapping for {}:", node_name);

        for (auto range : maybe_ranges.release_value()) {
            auto pci_address = TRY(range.child_bus_address().as<OpenFirmwareAddress>());
            auto cpu_physical_address = TRY(TRY(parent->translate_child_bus_address_to_root_address(range.parent_bus_address())).as_flatptr());
            auto range_size = TRY(range.length().as_size_t());

            static constexpr auto space_type_names = Array {
                "Configuration Space"sv,
                "I/O Space"sv,
                "32-bit-address Memory Space"sv,
                "64-bit-address Memory Space"sv
            };

            dbgln("  CPU {:p}-{:p} => PCI {:#016x}-{:#016x} {} prefetchable={} relocatable={}",
                cpu_physical_address,
                cpu_physical_address + range_size,
                pci_address.io_or_memory_space_address,
                pci_address.io_or_memory_space_address + range_size,
                space_type_names[to_underlying(pci_address.space_type)],
                !!pci_address.prefetchable,
                !pci_address.non_relocatable);

            if (pci_address.space_type != OpenFirmwareAddress::SpaceType::Memory32BitSpace
                && pci_address.space_type != OpenFirmwareAddress::SpaceType::Memory64BitSpace)
                continue; // We currently only support memory-mapped PCI on RISC-V and AArch64

            TRY(host_controller.add_memory_space_window(HostController::Window {
                .host_address = PhysicalAddress { cpu_physical_address },
                .bus_address = pci_address.io_or_memory_space_address,
                .size = range_size,
            }));

            if (pci_address.space_type == OpenFirmwareAddress::SpaceType::Memory32BitSpace) {
                if (pci_address.prefetchable)
                    continue; // We currently only use non-prefetchable 32-bit regions, since 64-bit regions are always prefetchable - TODO: Use 32-bit prefetchable regions if only they are available
                if (pci_32bit_mmio_size >= range_size)
                    continue; // We currently only use the single largest region - TODO: Use all available regions if needed

                pci_32bit_mmio_base = pci_address.io_or_memory_space_address;
                pci_32bit_mmio_size = range_size;
            } else {
                if (pci_64bit_mmio_size >= range_size)
                    continue; // We currently only use the single largest region - TODO: Use all available regions if needed

                pci_64bit_mmio_base = pci_address.io_or_memory_space_address;
                pci_64bit_mmio_size = range_size;
            }
        }
    }

    // 2.4.3 Interrupt Nexus Properties
    // #interrupt-cells: [2] `1` for pci busses
    // interrupt-map:
    //  [{
    //     child-unit-address(bus-node/#address-cells|3),
    //     child-interrupt-specifier(#interrupt-cells|1),
    //     interrupt-parent(phandle),
    //     parent-unit-address(interrupt-parent/#address-cells),
    //     parent-interrupt-specifier(interrupt-parent/#interrupt-cells)
    //  }]
    //   Note: The bus-node may be any other bus the child is connected to
    //   FIXME?: Let's just hope this is always this/a PCI bus
    // interrupt-map-mask:
    // > This property specifies a  mask that is ANDed with the incoming
    // > unit interrupt specifier being looked up in the table specified in the
    // > interrupt-map property.
    // Hence this should be of size:
    // pci/#address-cells(3) + #interrupt-cells(1) = 4
    auto maybe_interrupt_map = node.get_property("interrupt-map"sv);
    auto maybe_interrupt_map_mask = node.get_property("interrupt-map-mask"sv);
    if (maybe_interrupt_map.has_value() && maybe_interrupt_map_mask.has_value()) {
        auto maybe_interrupt_cells = node.get_property("#interrupt-cells"sv);
        if (!maybe_interrupt_cells.has_value())
            return EINVAL;

        if (maybe_interrupt_cells->size() != sizeof(u32) || maybe_interrupt_cells->as<u32>() != 1)
            return EINVAL;

        if (maybe_interrupt_map_mask->size() != 4 * sizeof(u32))
            return EINVAL;

        auto mask_stream = maybe_interrupt_map_mask.value().as_stream();
        auto pci_address_mask = TRY(mask_stream.read_value<OpenFirmwareAddress>());

        // The "interrupt-map-mask" constraints from https://github.com/devicetree-org/dt-schema/blob/main/dtschema/schemas/pci/pci-bus-common.yaml
        // only allow the function and device mask fields to be non-zero and the interrupt specifier mask to be between 0 and 7.
        if (pci_address_mask.register_ != 0
            || pci_address_mask.bus != 0
            || pci_address_mask.space_type != static_cast<OpenFirmwareAddress::SpaceType>(0)
            || pci_address_mask.aliased != 0
            || pci_address_mask.prefetchable != 0
            || pci_address_mask.non_relocatable != 0
            || pci_address_mask.io_or_memory_space_address != 0) {
            return EINVAL;
        }

        u32 pin_mask = TRY(mask_stream.read_cell());
        if (pin_mask > 7)
            return EINVAL;

        interrupt_mask = PCIInterruptSpecifier {
            .interrupt_pin = static_cast<u8>(pin_mask),
            .function = pci_address_mask.function,
            .device = pci_address_mask.device,
            .bus = pci_address_mask.bus,
        };
        auto map_stream = maybe_interrupt_map.value().as_stream();
        while (!map_stream.is_eof()) {
            auto pci_address = TRY(map_stream.read_value<OpenFirmwareAddress>());
            u32 pin = TRY(map_stream.read_cell());

            u32 interrupt_controller_phandle = TRY(map_stream.read_cell());
            auto const* interrupt_controller = device_tree.phandle(interrupt_controller_phandle);
            if (interrupt_controller == nullptr)
                return EINVAL;

            if (!interrupt_controller->has_property("interrupt-controller"sv)) {
                dmesgln("PCI: Implement support for nested interrupt nexuses");
                TODO();
            }

            // The devicetree spec says that we should assume `#address-cells = <2>` if the property isn't present.
            // Linux however defaults to `#address-cells = <0>` for interrupt parent nodes.
            // Some devicetrees seem to expect this Linux behavior, so we have to follow it.
            u32 interrupt_controller_address_cells = 0;

            if (auto prop = interrupt_controller->get_property("#address-cells"sv); prop.has_value())
                interrupt_controller_address_cells = prop.release_value().as<u32>();

            TRY(map_stream.discard(sizeof(u32) * interrupt_controller_address_cells));

            auto interrupt_cells = interrupt_controller->get_property("#interrupt-cells"sv)->as<u32>();
#if ARCH(RISCV64)
            if (interrupt_cells != 1 && interrupt_cells != 2)
                return ENOTSUP;

            u64 interrupt = TRY(map_stream.read_cells(interrupt_cells));
#elif ARCH(AARCH64)
            // FIXME: Don't depend on a specific interrupt descriptor format.
            auto const& domain_root = *TRY(interrupt_controller->interrupt_domain_root(device_tree));
            if (!domain_root.is_compatible_with("arm,gic-400"sv) && !domain_root.is_compatible_with("arm,cortex-a15-gic"sv))
                return ENOTSUP;

            if (interrupt_cells != 3)
                return ENOTSUP;

            TRY(map_stream.discard(sizeof(u32))); // This is the IRQ type.
            u64 interrupt = TRY(map_stream.read_cell()) + 32;
            TRY(map_stream.discard(sizeof(u32))); // This is the trigger type.
#else
#    error Unknown architecture
#endif

            pin &= pin_mask;
            pci_address &= pci_address_mask;
            masked_interrupt_mapping.set(
                PCIInterruptSpecifier {
                    .interrupt_pin = static_cast<u8>(pin),
                    .function = pci_address.function,
                    .device = pci_address.device,
                    .bus = pci_address.bus,
                },
                interrupt);
        }
    }

    if (pci_32bit_mmio_size != 0 || pci_64bit_mmio_size != 0) {
        PCIConfiguration config {
            pci_32bit_mmio_base,
            pci_32bit_mmio_base + pci_32bit_mmio_size,
            pci_64bit_mmio_base,
            pci_64bit_mmio_base + pci_64bit_mmio_size,
            move(masked_interrupt_mapping),
            interrupt_mask,
        };
        Access::the().configure_pci_space(host_controller, config);
    } else {
        dmesgln("PCI: No MMIO ranges found - assuming pre-configured by bootloader");
    }

    return {};
}

}