/* * 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 #include #include #include #include #include #include #include #include #include #include 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; }