blob: 5e3debdddc72e70e6cef3a40eece1e9fbe3e3467 [file] [log] [blame]
/*
* 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 ecam.rc;
/* 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 ) );
rc = -ENOENT;
goto err_find;
}
/* Map configuration space for this allocation */
base = le64_to_cpu ( ecam.alloc.base );
base += ( ecam.alloc.start * ECAM_SIZE * PCI_BUSDEVFN ( 0, 1, 0, 0 ) );
len = ( ecam.range.count * ECAM_SIZE );
if ( base != ( ( unsigned long ) base ) ) {
DBGC ( &ecam, "ECAM %04x:[%02x-%02x] could not map "
"[%08llx,%08llx) outside CPU range\n",
le16_to_cpu ( ecam.alloc.segment ), ecam.alloc.start,
ecam.alloc.end, base, ( base + len ) );
rc = -ERANGE;
goto err_range;
}
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 );
ecam.rc = 0;
return 0;
iounmap ( ecam.regs );
err_ioremap:
err_range:
err_find:
ecam.rc = rc;
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;
/* Write to 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 );
}
/* Read from address, to guarantee completion of the write
*
* PCIe configuration space registers may not have read side
* effects. Reading back is therefore always safe to do, and
* guarantees that the write has reached the device.
*/
mb();
ecam_read ( pci, location, &value );
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 );