summaryrefslogtreecommitdiffstats
path: root/plat/rpi/rpi4/rpi4_pci_svc.c
diff options
context:
space:
mode:
Diffstat (limited to 'plat/rpi/rpi4/rpi4_pci_svc.c')
-rw-r--r--plat/rpi/rpi4/rpi4_pci_svc.c215
1 files changed, 215 insertions, 0 deletions
diff --git a/plat/rpi/rpi4/rpi4_pci_svc.c b/plat/rpi/rpi4/rpi4_pci_svc.c
new file mode 100644
index 0000000..7d1ca5c
--- /dev/null
+++ b/plat/rpi/rpi4/rpi4_pci_svc.c
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2021, ARM Limited and Contributors. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * The RPi4 has a single nonstandard PCI config region. It is broken into two
+ * pieces, the root port config registers and a window to a single device's
+ * config space which can move between devices. There isn't (yet) an
+ * authoritative public document on this since the available BCM2711 reference
+ * notes that there is a PCIe root port in the memory map but doesn't describe
+ * it. Given that it's not ECAM compliant yet reasonably simple, it makes for
+ * an excellent example of the PCI SMCCC interface.
+ *
+ * The PCI SMCCC interface is described in DEN0115 availabe from:
+ * https://developer.arm.com/documentation/den0115/latest
+ */
+
+#include <assert.h>
+#include <stdint.h>
+
+#include <common/debug.h>
+#include <common/runtime_svc.h>
+#include <lib/pmf/pmf.h>
+#include <lib/runtime_instr.h>
+#include <services/pci_svc.h>
+#include <services/sdei.h>
+#include <services/std_svc.h>
+#include <smccc_helpers.h>
+
+#include <lib/mmio.h>
+
+static spinlock_t pci_lock;
+
+#define PCIE_REG_BASE U(RPI_IO_BASE + 0x01500000)
+#define PCIE_MISC_PCIE_STATUS 0x4068
+#define PCIE_EXT_CFG_INDEX 0x9000
+/* A small window pointing at the ECAM of the device selected by CFG_INDEX */
+#define PCIE_EXT_CFG_DATA 0x8000
+#define INVALID_PCI_ADDR 0xFFFFFFFF
+
+#define PCIE_EXT_BUS_SHIFT 20
+#define PCIE_EXT_DEV_SHIFT 15
+#define PCIE_EXT_FUN_SHIFT 12
+
+
+static uint64_t pci_segment_lib_get_base(uint32_t address, uint32_t offset)
+{
+ uint64_t base;
+ uint32_t bus, dev, fun;
+ uint32_t status;
+
+ base = PCIE_REG_BASE;
+
+ offset &= PCI_OFFSET_MASK; /* Pick off the 4k register offset */
+
+ /* The root port is at the base of the PCIe register space */
+ if (address != 0U) {
+ /*
+ * The current device must be at CFG_DATA, a 4K window mapped,
+ * via CFG_INDEX, to the device we are accessing. At the same
+ * time we must avoid accesses to certain areas of the cfg
+ * space via CFG_DATA. Detect those accesses and report that
+ * the address is invalid.
+ */
+ base += PCIE_EXT_CFG_DATA;
+ bus = PCI_ADDR_BUS(address);
+ dev = PCI_ADDR_DEV(address);
+ fun = PCI_ADDR_FUN(address);
+ address = (bus << PCIE_EXT_BUS_SHIFT) |
+ (dev << PCIE_EXT_DEV_SHIFT) |
+ (fun << PCIE_EXT_FUN_SHIFT);
+
+ /* Allow only dev = 0 on root port and bus 1 */
+ if ((bus < 2U) && (dev > 0U)) {
+ return INVALID_PCI_ADDR;
+ }
+
+ /* Assure link up before reading bus 1 */
+ status = mmio_read_32(PCIE_REG_BASE + PCIE_MISC_PCIE_STATUS);
+ if ((status & 0x30) != 0x30) {
+ return INVALID_PCI_ADDR;
+ }
+
+ /* Adjust which device the CFG_DATA window is pointing at */
+ mmio_write_32(PCIE_REG_BASE + PCIE_EXT_CFG_INDEX, address);
+ }
+ return base + offset;
+}
+
+/**
+ * pci_read_config() - Performs a config space read at addr
+ * @addr: 32-bit, segment, BDF of requested function encoded per DEN0115
+ * @off: register offset of function described by @addr to read
+ * @sz: size of read (8,16,32) bits.
+ * @val: returned zero extended value read from config space
+ *
+ * sz bits of PCI config space is read at addr:offset, and the value
+ * is returned in val. Invalid segment/offset values return failure.
+ * Reads to valid functions that don't exist return INVALID_PCI_ADDR
+ * as is specified by PCI for requests that aren't completed by EPs.
+ * The boilerplate in pci_svc.c tends to do basic segment, off
+ * and sz validation. This routine should avoid duplicating those
+ * checks.
+ *
+ * This function maps directly to the PCI_READ function in DEN0115
+ * where detailed requirements may be found.
+ *
+ * Return: SMC_PCI_CALL_SUCCESS with val set
+ * SMC_PCI_CALL_INVAL_PARAM, on parameter error
+ */
+uint32_t pci_read_config(uint32_t addr, uint32_t off, uint32_t sz, uint32_t *val)
+{
+ uint32_t ret = SMC_PCI_CALL_SUCCESS;
+ uint64_t base;
+
+ spin_lock(&pci_lock);
+ base = pci_segment_lib_get_base(addr, off);
+
+ if (base == INVALID_PCI_ADDR) {
+ *val = base;
+ } else {
+ switch (sz) {
+ case SMC_PCI_SZ_8BIT:
+ *val = mmio_read_8(base);
+ break;
+ case SMC_PCI_SZ_16BIT:
+ *val = mmio_read_16(base);
+ break;
+ case SMC_PCI_SZ_32BIT:
+ *val = mmio_read_32(base);
+ break;
+ default: /* should be unreachable */
+ *val = 0;
+ ret = SMC_PCI_CALL_INVAL_PARAM;
+ }
+ }
+ spin_unlock(&pci_lock);
+ return ret;
+}
+
+/**
+ * pci_write_config() - Performs a config space write at addr
+ * @addr: 32-bit, segment, BDF of requested function encoded per DEN0115
+ * @off: register offset of function described by @addr to write
+ * @sz: size of write (8,16,32) bits.
+ * @val: value to be written
+ *
+ * sz bits of PCI config space is written at addr:offset. Invalid
+ * segment/BDF values return failure. Writes to valid functions
+ * without valid EPs are ignored, as is specified by PCI.
+ * The boilerplate in pci_svc.c tends to do basic segment, off
+ * and sz validation, so it shouldn't need to be repeated here.
+ *
+ * This function maps directly to the PCI_WRITE function in DEN0115
+ * where detailed requirements may be found.
+ *
+ * Return: SMC_PCI_CALL_SUCCESS
+ * SMC_PCI_CALL_INVAL_PARAM, on parameter error
+ */
+uint32_t pci_write_config(uint32_t addr, uint32_t off, uint32_t sz, uint32_t val)
+{
+ uint32_t ret = SMC_PCI_CALL_SUCCESS;
+ uint64_t base;
+
+ spin_lock(&pci_lock);
+ base = pci_segment_lib_get_base(addr, off);
+
+ if (base != INVALID_PCI_ADDR) {
+ switch (sz) {
+ case SMC_PCI_SZ_8BIT:
+ mmio_write_8(base, val);
+ break;
+ case SMC_PCI_SZ_16BIT:
+ mmio_write_16(base, val);
+ break;
+ case SMC_PCI_SZ_32BIT:
+ mmio_write_32(base, val);
+ break;
+ default: /* should be unreachable */
+ ret = SMC_PCI_CALL_INVAL_PARAM;
+ }
+ }
+ spin_unlock(&pci_lock);
+ return ret;
+}
+
+/**
+ * pci_get_bus_for_seg() - returns the start->end bus range for a segment
+ * @seg: segment being queried
+ * @bus_range: returned bus begin + (end << 8)
+ * @nseg: returns next segment in this machine or 0 for end
+ *
+ * pci_get_bus_for_seg is called to check if a given segment is
+ * valid on this machine. If it is valid, then its bus ranges are
+ * returned along with the next valid segment on the machine. If
+ * this is the last segment, then nseg must be 0.
+ *
+ * This function maps directly to the PCI_GET_SEG_INFO function
+ * in DEN0115 where detailed requirements may be found.
+ *
+ * Return: SMC_PCI_CALL_SUCCESS, and appropriate bus_range and nseg
+ * SMC_PCI_CALL_NOT_IMPL, if the segment is invalid
+ */
+uint32_t pci_get_bus_for_seg(uint32_t seg, uint32_t *bus_range, uint32_t *nseg)
+{
+ uint32_t ret = SMC_PCI_CALL_SUCCESS;
+ *nseg = 0U; /* only a single segment */
+ if (seg == 0U) {
+ *bus_range = 0xFF00; /* start 0, end 255 */
+ } else {
+ *bus_range = 0U;
+ ret = SMC_PCI_CALL_NOT_IMPL;
+ }
+ return ret;
+}