| /* |
| * Copyright (C) 2022 Michael Brown <mbrown@fensystems.co.uk>. |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License as |
| * published by the Free Software Foundation; either version 2 of the |
| * License, or (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| * 02110-1301, USA. |
| * |
| * You can also choose to distribute this program under the terms of |
| * the Unmodified Binary Distribution Licence (as given in the file |
| * COPYING.UBDL), provided that you have satisfied its requirements. |
| */ |
| |
| FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); |
| |
| #include <errno.h> |
| #include <ipxe/uaccess.h> |
| #include <ipxe/ecam.h> |
| |
| /** @file |
| * |
| * PCI Enhanced Configuration Access Mechanism (ECAM) |
| * |
| */ |
| |
| /** Cached mapped ECAM allocation */ |
| static struct ecam_mapping ecam; |
| |
| /** |
| * Find lowest ECAM allocation not below a given PCI bus:dev.fn address |
| * |
| * @v busdevfn PCI bus:dev.fn address |
| * @v range PCI device address range to fill in |
| * @v alloc ECAM allocation to fill in, or NULL |
| * @ret rc Return status code |
| */ |
| static int ecam_find ( uint32_t busdevfn, struct pci_range *range, |
| struct ecam_allocation *alloc ) { |
| struct ecam_allocation tmp; |
| unsigned int best = 0; |
| unsigned int offset; |
| unsigned int count; |
| unsigned int index; |
| userptr_t mcfg; |
| uint32_t length; |
| uint32_t start; |
| |
| /* Return empty range on error */ |
| range->count = 0; |
| |
| /* Locate MCFG table */ |
| mcfg = acpi_table ( ECAM_SIGNATURE, 0 ); |
| if ( ! mcfg ) { |
| DBGC ( &ecam, "ECAM found no MCFG table\n" ); |
| return -ENOTSUP; |
| } |
| |
| /* Get length of table */ |
| copy_from_user ( &length, mcfg, |
| offsetof ( struct ecam_table, acpi.length ), |
| sizeof ( length ) ); |
| |
| /* Iterate over allocations */ |
| for ( offset = offsetof ( struct ecam_table, alloc ) ; |
| ( offset + sizeof ( tmp ) ) <= le32_to_cpu ( length ) ; |
| offset += sizeof ( tmp ) ) { |
| |
| /* Read allocation */ |
| copy_from_user ( &tmp, mcfg, offset, sizeof ( tmp ) ); |
| DBGC2 ( &ecam, "ECAM %04x:[%02x-%02x] has base %08llx\n", |
| le16_to_cpu ( tmp.segment ), tmp.start, tmp.end, |
| ( ( unsigned long long ) le64_to_cpu ( tmp.base ) ) ); |
| start = PCI_BUSDEVFN ( le16_to_cpu ( tmp.segment ), |
| tmp.start, 0, 0 ); |
| count = PCI_BUSDEVFN ( 0, ( tmp.end - tmp.start + 1 ), 0, 0 ); |
| |
| /* Check for a matching or new closest allocation */ |
| index = ( busdevfn - start ); |
| if ( ( index < count ) || ( index > best ) ) { |
| if ( alloc ) |
| memcpy ( alloc, &tmp, sizeof ( *alloc ) ); |
| range->start = start; |
| range->count = count; |
| best = index; |
| } |
| |
| /* Stop if this range contains the target bus:dev.fn address */ |
| if ( index < count ) |
| return 0; |
| } |
| |
| return ( best ? 0 : -ENOENT ); |
| } |
| |
| /** |
| * Find next PCI bus:dev.fn address range in system |
| * |
| * @v busdevfn Starting PCI bus:dev.fn address |
| * @v range PCI bus:dev.fn address range to fill in |
| */ |
| static void ecam_discover ( uint32_t busdevfn, struct pci_range *range ) { |
| |
| /* Find new range, if any */ |
| ecam_find ( busdevfn, range, NULL ); |
| } |
| |
| /** |
| * Access configuration space for PCI device |
| * |
| * @v pci PCI device |
| * @ret rc Return status code |
| */ |
| static int ecam_access ( struct pci_device *pci ) { |
| uint64_t base; |
| size_t len; |
| int rc; |
| |
| /* Reuse mapping if possible */ |
| if ( ( pci->busdevfn - ecam.range.start ) < ecam.range.count ) |
| return 0; |
| |
| /* Clear any existing mapping */ |
| if ( ecam.regs ) { |
| iounmap ( ecam.regs ); |
| ecam.regs = NULL; |
| } |
| |
| /* Find allocation for this PCI device */ |
| if ( ( rc = ecam_find ( pci->busdevfn, &ecam.range, |
| &ecam.alloc ) ) != 0 ) { |
| DBGC ( &ecam, "ECAM found no allocation for " PCI_FMT ": %s\n", |
| PCI_ARGS ( pci ), strerror ( rc ) ); |
| goto err_find; |
| } |
| if ( ecam.range.start > pci->busdevfn ) { |
| DBGC ( &ecam, "ECAM found no allocation for " PCI_FMT "\n", |
| PCI_ARGS ( pci ) ); |
| goto err_find; |
| } |
| |
| /* Map configuration space for this allocation */ |
| base = le64_to_cpu ( ecam.alloc.base ); |
| len = ( ecam.range.count * ECAM_SIZE ); |
| ecam.regs = ioremap ( base, len ); |
| if ( ! ecam.regs ) { |
| DBGC ( &ecam, "ECAM %04x:[%02x-%02x] could not map " |
| "[%08llx,%08llx)\n", le16_to_cpu ( ecam.alloc.segment ), |
| ecam.alloc.start, ecam.alloc.end, base, ( base + len ) ); |
| rc = -ENODEV; |
| goto err_ioremap; |
| } |
| |
| /* Populate cached mapping */ |
| DBGC ( &ecam, "ECAM %04x:[%02x-%02x] mapped [%08llx,%08llx) -> %p\n", |
| le16_to_cpu ( ecam.alloc.segment ), ecam.alloc.start, |
| ecam.alloc.end, base, ( base + len ), ecam.regs ); |
| return 0; |
| |
| iounmap ( ecam.regs ); |
| err_ioremap: |
| err_find: |
| ecam.range.count = 0; |
| return rc; |
| } |
| |
| /** |
| * Read from PCI configuration space |
| * |
| * @v pci PCI device |
| * @v location Offset and length within PCI configuration space |
| * @v value Value read |
| * @ret rc Return status code |
| */ |
| int ecam_read ( struct pci_device *pci, unsigned int location, void *value ) { |
| unsigned int where = ECAM_WHERE ( location ); |
| unsigned int len = ECAM_LEN ( location ); |
| unsigned int index; |
| void *addr; |
| int rc; |
| |
| /* Return all-ones on error */ |
| memset ( value, 0xff, len ); |
| |
| /* Access configuration space */ |
| if ( ( rc = ecam_access ( pci ) ) != 0 ) |
| return rc; |
| |
| /* Read from address */ |
| index = ( pci->busdevfn - ecam.range.start ); |
| addr = ( ecam.regs + ( index * ECAM_SIZE ) + where ); |
| switch ( len ) { |
| case 4: |
| *( ( uint32_t *) value ) = readl ( addr ); |
| break; |
| case 2: |
| *( ( uint16_t *) value ) = readw ( addr ); |
| break; |
| case 1: |
| *( ( uint8_t *) value ) = readb ( addr ); |
| break; |
| default: |
| assert ( 0 ); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Write to PCI configuration space |
| * |
| * @v pci PCI device |
| * @v location Offset and length within PCI configuration space |
| * @v value Value to write |
| * @ret rc Return status code |
| */ |
| int ecam_write ( struct pci_device *pci, unsigned int location, |
| unsigned long value ) { |
| unsigned int where = ECAM_WHERE ( location ); |
| unsigned int len = ECAM_LEN ( location ); |
| unsigned int index; |
| void *addr; |
| int rc; |
| |
| /* Access configuration space */ |
| if ( ( rc = ecam_access ( pci ) ) != 0 ) |
| return rc; |
| |
| /* Read from address */ |
| index = ( pci->busdevfn - ecam.range.start ); |
| addr = ( ecam.regs + ( index * ECAM_SIZE ) + where ); |
| switch ( len ) { |
| case 4: |
| writel ( value, addr ); |
| break; |
| case 2: |
| writew ( value, addr ); |
| break; |
| case 1: |
| writeb ( value, addr ); |
| break; |
| default: |
| assert ( 0 ); |
| } |
| |
| return 0; |
| } |
| |
| PROVIDE_PCIAPI ( ecam, pci_discover, ecam_discover ); |
| PROVIDE_PCIAPI_INLINE ( ecam, pci_read_config_byte ); |
| PROVIDE_PCIAPI_INLINE ( ecam, pci_read_config_word ); |
| PROVIDE_PCIAPI_INLINE ( ecam, pci_read_config_dword ); |
| PROVIDE_PCIAPI_INLINE ( ecam, pci_write_config_byte ); |
| PROVIDE_PCIAPI_INLINE ( ecam, pci_write_config_word ); |
| PROVIDE_PCIAPI_INLINE ( ecam, pci_write_config_dword ); |
| PROVIDE_PCIAPI_INLINE ( ecam, pci_ioremap ); |
| |
| struct pci_api ecam_api = PCIAPI_RUNTIME ( ecam ); |