/*
 * Copyright (C) 2012 Adrian Jamróz <adrian.jamroz@gmail.com>
 *
 * 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.
 */

FILE_LICENCE ( GPL2_OR_LATER );

#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <byteswap.h>
#include <ipxe/netdevice.h>
#include <ipxe/ethernet.h>
#include <ipxe/if_ether.h>
#include <ipxe/iobuf.h>
#include <ipxe/malloc.h>
#include <ipxe/pci.h>
#include <ipxe/mii.h>
#include "velocity.h"

#define	velocity_setbit(_reg, _mask)	writeb ( readb ( _reg ) | _mask, _reg )
#define virt_to_le32bus(x)		( cpu_to_le32 ( virt_to_bus ( x ) ) )

/** @file
 *
 * VIA Velocity network driver
 *
 */

/******************************************************************************
 *
 * MII interface
 *
 ******************************************************************************
 */

/**
 * Stop MII auto-polling
 *
 * @v vlc	Velocity device
 * @ret	rc	Return status code
 */
static int velocity_autopoll_stop ( struct velocity_nic *vlc ) {
	int timeout = VELOCITY_TIMEOUT_US;

	/* Disable MII auto polling */
	writeb ( 0, vlc->regs + VELOCITY_MIICR );

	/* Wait for disabling to take effect */
	while ( timeout-- ) {
		udelay ( 1 );
		if ( readb ( vlc->regs + VELOCITY_MIISR ) &
		             VELOCITY_MIISR_IDLE )
			return 0;
	}

	DBGC ( vlc, "MII autopoll stop timeout\n" );
	return -ETIMEDOUT;
}

/**
 * Start MII auto-polling
 *
 * @v vlc	Velocity device
 * @ret rc	Return status code
 */
static int velocity_autopoll_start ( struct velocity_nic *vlc ) {
	int timeout = VELOCITY_TIMEOUT_US;

	/* Enable MII auto polling */
	writeb ( VELOCITY_MIICR_MAUTO, vlc->regs + VELOCITY_MIICR );

	/* Wait for enabling to take effect */
	while ( timeout-- ) {
		udelay ( 1 );
		if ( ( readb ( vlc->regs + VELOCITY_MIISR ) &
		       VELOCITY_MIISR_IDLE ) == 0 )
			return 0;
	}

	DBGC ( vlc, "MII autopoll start timeout\n" );
	return -ETIMEDOUT;
}

/**
 * Read from MII register
 *
 * @v mdio		MII interface
 * @v phy		PHY address
 * @v reg		Register address
 * @ret value		Data read, or negative error
 */
static int velocity_mii_read ( struct mii_interface *mdio,
			       unsigned int phy __unused, unsigned int reg ) {
	struct velocity_nic *vlc =
		container_of ( mdio, struct velocity_nic, mdio );
	int timeout = VELOCITY_TIMEOUT_US;
	int result;

	DBGC2 ( vlc, "VELOCITY %p MII read reg %d\n", vlc, reg );

	/* Disable autopolling before we can access MII */
	velocity_autopoll_stop ( vlc );

	/* Send read command and address */
	writeb ( reg, vlc->regs + VELOCITY_MIIADDR );
	velocity_setbit ( vlc->regs + VELOCITY_MIICR, VELOCITY_MIICR_RCMD );

	/* Wait for read to complete */
	while ( timeout-- ) {
		udelay ( 1 );
		if ( ( readb ( vlc->regs + VELOCITY_MIICR ) &
		       VELOCITY_MIICR_RCMD ) == 0 ) {
			result = readw ( vlc->regs + VELOCITY_MIIDATA );
			velocity_autopoll_start ( vlc );
			return result;
		}
	}

	/* Restart autopolling */
	velocity_autopoll_start ( vlc );

	DBGC ( vlc, "MII read timeout\n" );
	return -ETIMEDOUT;
}

/**
 * Write to MII register
 *
 * @v mdio		MII interface
 * @v phy		PHY address
 * @v reg		Register address
 * @v data		Data to write
 * @ret rc		Return status code
 */
static int velocity_mii_write ( struct mii_interface *mdio,
				unsigned int phy __unused, unsigned int reg,
				unsigned int data) {
	struct velocity_nic *vlc =
		container_of ( mdio, struct velocity_nic, mdio );
	int timeout = VELOCITY_TIMEOUT_US;

	DBGC2 ( vlc, "VELOCITY %p MII write reg %d data 0x%04x\n",
			vlc, reg, data );

	/* Disable autopolling before we can access MII */
	velocity_autopoll_stop ( vlc );

	/* Send write command, data and destination register */
	writeb ( reg, vlc->regs + VELOCITY_MIIADDR );
	writew ( data, vlc->regs + VELOCITY_MIIDATA );
	velocity_setbit ( vlc->regs + VELOCITY_MIICR, VELOCITY_MIICR_WCMD );

	/* Wait for write to complete */
	while ( timeout-- ) {
		udelay ( 1 );
		if ( ( readb ( vlc->regs + VELOCITY_MIICR ) &
		       VELOCITY_MIICR_WCMD ) == 0 ) {
			velocity_autopoll_start ( vlc );
			return 0;
		}
	}

	/* Restart autopolling */
	velocity_autopoll_start ( vlc );

	DBGC ( vlc, "MII write timeout\n" );
	return -ETIMEDOUT;
}

/** Velocity MII operations */
static struct mii_operations velocity_mii_operations = {
	.read = velocity_mii_read,
	.write = velocity_mii_write,
};

/**
 * Set Link speed 
 *
 * @v vlc	Velocity device
 */
static void velocity_set_link ( struct velocity_nic *vlc ) {
	int tmp;

	/* Advertise 1000MBit */
	tmp = mii_read ( &vlc->mii, MII_CTRL1000 );
	tmp |= ADVERTISE_1000FULL | ADVERTISE_1000HALF;
	mii_write ( &vlc->mii, MII_CTRL1000, tmp );

	/* Enable GBit operation in MII Control Register */
	tmp = mii_read ( &vlc->mii, MII_BMCR );
	tmp |= BMCR_SPEED1000;
	mii_write ( &vlc->mii, MII_BMCR, tmp );
}

/******************************************************************************
 *
 * Device reset
 *
 ******************************************************************************
 */

/**
 * Reload eeprom contents
 *
 * @v vlc		Velocity device
 */
static int velocity_reload_eeprom ( struct velocity_nic *vlc ) {
	int timeout = VELOCITY_TIMEOUT_US;

	/* Initiate reload */
	velocity_setbit ( vlc->regs + VELOCITY_EECSR, VELOCITY_EECSR_RELOAD );

	/* Wait for reload to complete */
	while ( timeout-- ) {
		udelay ( 1 );
		if ( ( readb ( vlc->regs + VELOCITY_EECSR ) &
		       VELOCITY_EECSR_RELOAD ) == 0 )
			return 0;
	}

	DBGC ( vlc, "VELOCITY %p EEPROM reload timeout\n", vlc );
	return -ETIMEDOUT;
}

/**
 * Reset hardware
 *
 * @v vlc		Velocity device
 * @ret rc		Return status code
 */
static int velocity_reset ( struct velocity_nic *vlc ) {
	int timeout = VELOCITY_TIMEOUT_US;
	uint8_t tmp;

	DBGC ( vlc, "VELOCITY %p reset\n", vlc );

	/* clear sticky Power state bits */
	tmp = readb ( vlc->regs + VELOCITY_STICKY );
	tmp &= ~( VELOCITY_STICKY_DS0 | VELOCITY_STICKY_DS1 );
	writeb ( tmp, vlc->regs + VELOCITY_STICKY );

	/* clear PACPI, which might have been enabled by the EEPROM reload */
	tmp = readb ( vlc->regs + VELOCITY_CFGA );
	tmp &= ~VELOCITY_CFGA_PACPI;
	writeb ( tmp, vlc->regs + VELOCITY_CFGA );

	velocity_setbit ( vlc->regs + VELOCITY_CRS1, VELOCITY_CR1_SFRST );

	/* Wait for reset to complete */
	while ( timeout-- ) {
		udelay ( 1 );
		if ( ( readb ( vlc->regs + VELOCITY_CRS1 ) &
		       VELOCITY_CR1_SFRST ) == 0 )
			return 0;
	}

	return -EINVAL;
}

/******************************************************************************
 *
 * Link state
 *
 ******************************************************************************
 */

/**
 * Check link state
 *
 * @v netdev		Network device
 */
static void velocity_check_link ( struct net_device *netdev ) {
	struct velocity_nic *vlc = netdev->priv;

	if ( readb ( vlc->regs + VELOCITY_PHYSTS0 ) & VELOCITY_PHYSTS0_LINK ) {
		netdev_link_up ( netdev );
		DBGC ( vlc, "VELOCITY %p link up\n", vlc );
	} else {
		netdev_link_down ( netdev );
		DBGC ( vlc, "VELOCITY %p link down\n", vlc );
	}

	/* The card disables auto-poll after a link change */
	velocity_autopoll_start ( vlc );
}

/******************************************************************************
 *
 * Network device interface
 *
 ******************************************************************************
 */

/**
 * Allocate descriptor rings
 *
 * @v vlc	Velocity device
 * @ret rc	Return status code
 */
static int velocity_alloc_rings ( struct velocity_nic *vlc ) {
	int rc = 0;

	/* Allocate RX descriptor ring */
	vlc->rx_prod = 0;
	vlc->rx_cons = 0;
	vlc->rx_commit = 0;
	vlc->rx_ring = malloc_dma ( VELOCITY_RXDESC_SIZE, VELOCITY_RING_ALIGN );
	if ( ! vlc->rx_ring )
		return -ENOMEM;

	memset ( vlc->rx_ring, 0, VELOCITY_RXDESC_SIZE );

	DBGC2 ( vlc, "VELOCITY %p RX ring start address: %p(phys: %#08lx)\n",
	       vlc, vlc->rx_ring, virt_to_bus ( vlc->rx_ring ) );

	/* Allocate TX descriptor ring */
	vlc->tx_prod = 0;
	vlc->tx_cons = 0;
	vlc->tx_ring = malloc_dma ( VELOCITY_TXDESC_SIZE, VELOCITY_RING_ALIGN );
	if ( ! vlc->tx_ring ) {
		rc = -ENOMEM;
		goto err_tx_alloc;
	}

	memset ( vlc->tx_ring, 0, VELOCITY_TXDESC_SIZE );

	/* Send RX ring to the card */
	writel ( virt_to_bus ( vlc->rx_ring ),
	         vlc->regs + VELOCITY_RXDESC_ADDR_LO );
	writew ( VELOCITY_RXDESC_NUM - 1, vlc->regs + VELOCITY_RXDESCNUM );

	/* Send TX ring to the card */
	writel ( virt_to_bus ( vlc->tx_ring ),
	         vlc->regs + VELOCITY_TXDESC_ADDR_LO0 );
	writew ( VELOCITY_TXDESC_NUM - 1, vlc->regs + VELOCITY_TXDESCNUM );

	DBGC2 ( vlc, "VELOCITY %p TX ring start address: %p(phys: %#08lx)\n",
	       vlc, vlc->tx_ring, virt_to_bus ( vlc->tx_ring ) );

	return 0;

err_tx_alloc:
	free_dma ( vlc->rx_ring, VELOCITY_RXDESC_SIZE );
	return rc;
}

/**
 * Refill receive descriptor ring
 *
 * @v vlc	Velocity device
 */
static void velocity_refill_rx ( struct velocity_nic *vlc ) {
	struct velocity_rx_descriptor *desc;
	struct io_buffer *iobuf;
	int rx_idx, i = 0;

	/* Check for new packets */
	while ( ( vlc->rx_prod - vlc->rx_cons ) < VELOCITY_RXDESC_NUM ) {
		iobuf = alloc_iob ( VELOCITY_RX_MAX_LEN );

		/* Memory pressure: try again next poll */
		if ( ! iobuf )
			break;

		rx_idx = ( vlc->rx_prod++ % VELOCITY_RXDESC_NUM );
		desc = &vlc->rx_ring[rx_idx];

		/* Set descrptor fields */
		desc->des1 = 0;
		desc->addr = virt_to_le32bus ( iobuf-> data );
		desc->des2 = cpu_to_le32 (
		    VELOCITY_DES2_SIZE ( VELOCITY_RX_MAX_LEN - 1 ) |
		    VELOCITY_DES2_IC );

		vlc->rx_buffs[rx_idx] = iobuf;
		i++;

		/* Return RX descriptors in blocks of 4 (hw requirement) */
		if ( rx_idx % 4 == 3 ) {
			int j;
			for (j = 0; j < 4; j++) {
				desc = &vlc->rx_ring[rx_idx - j];
				desc->des0 = cpu_to_le32 ( VELOCITY_DES0_OWN );
			}
			vlc->rx_commit += 4;
		}
	}

	wmb();

	if ( vlc->rx_commit ) {
		writew ( vlc->rx_commit,
		         vlc->regs + VELOCITY_RXDESC_RESIDUECNT );
		vlc->rx_commit = 0;
	}

	if ( i > 0 )
		DBGC2 ( vlc, "VELOCITY %p refilled %d RX descriptors\n",
		        vlc, i );
}

/**
 * Open network device
 *
 * @v netdev		Network device
 * @ret rc		Return status code
 */
static int velocity_open ( struct net_device *netdev ) {
	struct velocity_nic *vlc = netdev->priv;
	int rc;

	DBGC ( vlc, "VELOCITY %p open\n", vlc );
	DBGC ( vlc, "VELOCITY %p regs at: %p\n", vlc, vlc->regs );

	/* Allocate descriptor rings */
	if ( ( rc = velocity_alloc_rings ( vlc ) ) != 0 )
		return rc;

	velocity_refill_rx ( vlc );

	/* Enable TX/RX queue */
	writew ( VELOCITY_TXQCSRS_RUN0, vlc->regs + VELOCITY_TXQCSRS );
	writew ( VELOCITY_RXQCSR_RUN | VELOCITY_RXQCSR_WAK,
	         vlc->regs + VELOCITY_RXQCSRS );

	/* Enable interrupts */
	writeb ( 0xff, vlc->regs + VELOCITY_IMR0 );
	writeb ( 0xff, vlc->regs + VELOCITY_IMR1 );

	/* Start MAC */
	writeb ( VELOCITY_CR0_STOP, vlc->regs + VELOCITY_CRC0 );
	writeb ( VELOCITY_CR1_DPOLL, vlc->regs + VELOCITY_CRC0 );
	writeb ( VELOCITY_CR0_START | VELOCITY_CR0_TXON | VELOCITY_CR0_RXON,
	         vlc->regs + VELOCITY_CRS0 );

	/* Receive all packets */
	writeb ( 0xff, vlc->regs + VELOCITY_RCR );

	/* Set initial link state */
	velocity_check_link ( netdev );

	velocity_autopoll_start ( vlc );

	DBGC2 ( vlc, "VELOCITY %p CR3 %02x\n",
	        vlc, readb ( vlc->regs + 0x0B ) );

	return 0;
}

/**
 * Close network device
 *
 * @v netdev		Network device
 */
static void velocity_close ( struct net_device *netdev ) {
	struct velocity_nic *vlc = netdev->priv;
	int i;

	/* Stop NIC */
	writeb ( VELOCITY_CR0_TXON | VELOCITY_CR0_RXON,
	         vlc->regs + VELOCITY_CRC0 );
	writeb ( VELOCITY_CR0_STOP, vlc->regs + VELOCITY_CRS0 );

	/* Clear RX ring information */
	writel ( 0, vlc->regs + VELOCITY_RXDESC_ADDR_LO );
	writew ( 0, vlc->regs + VELOCITY_RXDESCNUM );

	/* Destroy RX ring */
	free_dma ( vlc->rx_ring, VELOCITY_RXDESC_SIZE );
	vlc->rx_ring = NULL;
	vlc->rx_prod = 0;
	vlc->rx_cons = 0;

	/* Discard receive buffers */
	for ( i = 0 ; i < VELOCITY_RXDESC_NUM ; i++ ) {
		if ( vlc->rx_buffs[i] )
			free_iob ( vlc->rx_buffs[i] );
		vlc->rx_buffs[i] = NULL;
	}

	/* Clear TX ring information */
	writel ( 0, vlc->regs + VELOCITY_TXDESC_ADDR_LO0 );
	writew ( 0, vlc->regs + VELOCITY_TXDESCNUM );

	/* Destroy TX ring */
	free_dma ( vlc->tx_ring, VELOCITY_TXDESC_SIZE );
	vlc->tx_ring = NULL;
	vlc->tx_prod = 0;
	vlc->tx_cons = 0;
}

/**
 * Transmit packet
 *
 * @v netdev		Network device
 * @v iobuf		I/O buffer
 * @ret rc		Return status code
 */
static int velocity_transmit ( struct net_device *netdev,
			       struct io_buffer *iobuf ) {
	struct velocity_nic *vlc = netdev->priv;
	struct velocity_tx_descriptor *desc;
	unsigned int tx_idx;

	/* Pad packet to minimum length */
	iob_pad ( iobuf, ETH_ZLEN );

	tx_idx = ( vlc->tx_prod++ % VELOCITY_TXDESC_NUM );
	desc = &vlc->tx_ring[tx_idx];

	/* Set packet size and transfer ownership to NIC */
	desc->des0 = cpu_to_le32 ( VELOCITY_DES0_OWN |
	                           VELOCITY_DES2_SIZE ( iob_len ( iobuf ) ) );
	/* Data in first desc fragment, only desc for packet, generate INT */
	desc->des1 = cpu_to_le32 ( VELOCITY_DES1_FRAG ( 1 ) |
	                           VELOCITY_DES1_TCPLS |
				   VELOCITY_DES1_INTR );

	desc->frags[0].addr = virt_to_le32bus ( iobuf->data );
	desc->frags[0].des2 = cpu_to_le32 (
	                      VELOCITY_DES2_SIZE ( iob_len ( iobuf ) ) );

	wmb();

	/* Initiate TX */
	velocity_setbit ( vlc->regs + VELOCITY_TXQCSRS, VELOCITY_TXQCSRS_WAK0 );

	DBGC2 ( vlc, "VELOCITY %p tx_prod=%d desc=%p iobuf=%p len=%zd\n",
	        vlc, tx_idx, desc, iobuf->data, iob_len ( iobuf ) );

	return 0;
}

/**
 * Poll for received packets.
 *
 * @v vlc	Velocity device
 */
static void velocity_poll_rx ( struct velocity_nic *vlc ) {
	struct velocity_rx_descriptor *desc;
	struct io_buffer *iobuf;
	int rx_idx;
	size_t len;
	uint32_t des0;

	/* Check for packets */
	while ( vlc->rx_cons != vlc->rx_prod ) {
		rx_idx = ( vlc->rx_cons % VELOCITY_RXDESC_NUM );
		desc = &vlc->rx_ring[rx_idx];

		des0 = cpu_to_le32 ( desc->des0 );

		/* Return if descriptor still in use */
		if ( des0 & VELOCITY_DES0_OWN )
			return;

		iobuf = vlc->rx_buffs[rx_idx];

		/* Get length, strip CRC */
		len = VELOCITY_DES0_RMBC ( des0 ) - 4;
		iob_put ( iobuf, len );

		DBGC2 ( vlc, "VELOCITY %p got packet on idx=%d (prod=%d), len %zd\n",
		    vlc, rx_idx, vlc->rx_prod % VELOCITY_RXDESC_NUM, len );

		if ( des0 & VELOCITY_DES0_RX_ERR ) {
			/* Report receive error */
			netdev_rx_err ( vlc->netdev, iobuf, -EINVAL );
			DBGC ( vlc, "VELOCITY %p receive error, status: %02x\n",
			       vlc, des0 );
		} else if ( des0 & VELOCITY_DES0_RXOK ) {
			/* Report receive success */
			netdev_rx( vlc->netdev, iobuf );
		} else {
			/* Card indicated neither success nor failure
			 * Technically this shouldn't happen, but we saw it
			 * in debugging once. */
			DBGC ( vlc, "VELOCITY %p RX neither ERR nor OK: %04x\n",
			       vlc, des0 );
			DBGC ( vlc, "packet len: %zd\n", len );
			DBGC_HD ( vlc, iobuf->data, 64 );

			/* we don't know what it is, treat is as an error */
			netdev_rx_err ( vlc->netdev, iobuf, -EINVAL );
		}

		vlc->rx_cons++;
	}
}

/**
 * Poll for completed packets.
 *
 * @v vlc	Velocity device
 */
static void velocity_poll_tx ( struct velocity_nic *vlc ) {
	struct velocity_tx_descriptor *desc;
	int tx_idx;

	/* Check for packets */
	while ( vlc->tx_cons != vlc->tx_prod ) {
		tx_idx = ( vlc->tx_cons % VELOCITY_TXDESC_NUM );
		desc = &vlc->tx_ring[tx_idx];

		/* Return if descriptor still in use */
		if ( le32_to_cpu ( desc->des0 ) & VELOCITY_DES0_OWN )
			return;

		/* Report errors */
		if ( le32_to_cpu ( desc->des0 ) & VELOCITY_DES0_TERR ) {
			netdev_tx_complete_next_err ( vlc->netdev, -EINVAL );
			return;
		}

		netdev_tx_complete_next ( vlc->netdev );

		DBGC2 ( vlc, "VELOCITY %p poll_tx cons=%d prod=%d tsr=%04x\n",
		        vlc, tx_idx, vlc->tx_prod % VELOCITY_TXDESC_NUM,
			( desc->des0 & 0xffff ) );
		vlc->tx_cons++;
	}
}

/**
 * Poll for completed and received packets
 *
 * @v netdev		Network device
 */
static void velocity_poll ( struct net_device *netdev ) {
	struct velocity_nic *vlc = netdev->priv;
	uint8_t isr1;

	isr1 = readb ( vlc->regs + VELOCITY_ISR1 );

	/* ACK interrupts */
	writew ( 0xFFFF, vlc->regs + VELOCITY_ISR0 );

	/* Check for competed packets */
	velocity_poll_rx ( vlc );
	velocity_poll_tx ( vlc );

	if ( isr1 & VELOCITY_ISR1_SRCI ) {
		/* Update linkstate */
		DBGC2 ( vlc, "VELOCITY %p link status interrupt\n", vlc );
		velocity_check_link ( netdev );
	}

	velocity_refill_rx ( vlc );

	/* deal with potential RX stall caused by RX ring underrun */
	writew ( VELOCITY_RXQCSR_RUN | VELOCITY_RXQCSR_WAK,
	         vlc->regs + VELOCITY_RXQCSRS );
}

/**
 * Enable or disable interrupts
 *
 * @v netdev		Network device
 * @v enable		Interrupts should be enabled
 */
static void velocity_irq ( struct net_device *netdev, int enable ) {
	struct velocity_nic *vlc = netdev->priv;

	DBGC ( vlc, "VELOCITY %p interrupts %s\n", vlc,
	    enable ? "enable" : "disable" );

	if (enable) {
		/* Enable interrupts */
		writeb ( VELOCITY_CR3_GINTMSK1, vlc->regs + VELOCITY_CRS3 );
	} else {
		/* Disable interrupts */
		writeb ( VELOCITY_CR3_GINTMSK1, vlc->regs + VELOCITY_CRC3 );
	}
}

/** Velocity network device operations */
static struct net_device_operations velocity_operations = {
	.open		= velocity_open,
	.close		= velocity_close,
	.transmit	= velocity_transmit,
	.poll		= velocity_poll,
	.irq		= velocity_irq,
};

/******************************************************************************
 *
 * PCI interface
 *
 ******************************************************************************
 */

/**
 * Probe PCI device
 *
 * @v pci		PCI device
 * @ret rc		Return status code
 */
static int velocity_probe ( struct pci_device *pci ) {
	struct net_device *netdev;
	struct velocity_nic *vlc;
	int rc;

	/* Allocate and initialise net device */
	netdev = alloc_etherdev ( sizeof ( *vlc ) );
	if ( ! netdev ) {
		rc = -ENOMEM;
		goto err_alloc;
	}
	netdev_init ( netdev, &velocity_operations );
	vlc = netdev->priv;
	pci_set_drvdata ( pci, netdev );
	netdev->dev = &pci->dev;

	/* Fix up PCI device */
	adjust_pci_device ( pci );

	/* Map registers */
	vlc->regs = ioremap ( pci->membase, VELOCITY_BAR_SIZE );
	vlc->netdev = netdev;

	/* Reset the NIC */
	if ( ( rc = velocity_reset ( vlc ) ) != 0 )
		goto err_reset;

	/* Reload EEPROM */
	if ( ( rc = velocity_reload_eeprom ( vlc ) ) != 0 )
		goto err_reset;

	/* Get MAC address */
	netdev->hw_addr[0] = readb ( vlc->regs + VELOCITY_MAC0 );
	netdev->hw_addr[1] = readb ( vlc->regs + VELOCITY_MAC1 );
	netdev->hw_addr[2] = readb ( vlc->regs + VELOCITY_MAC2 );
	netdev->hw_addr[3] = readb ( vlc->regs + VELOCITY_MAC3 );
	netdev->hw_addr[4] = readb ( vlc->regs + VELOCITY_MAC4 );
	netdev->hw_addr[5] = readb ( vlc->regs + VELOCITY_MAC5 );

	/* Initialise and reset MII interface */
	mdio_init ( &vlc->mdio, &velocity_mii_operations );
	mii_init ( &vlc->mii, &vlc->mdio, 0 );
	if ( ( rc = mii_reset ( &vlc->mii ) ) != 0 ) {
		DBGC ( vlc, "VELOCITY %p could not reset MII: %s\n",
		       vlc, strerror ( rc ) );
		goto err_mii_reset;
	}

	/* Enable proper link advertising */
	velocity_set_link ( vlc );

	/* Register network device */
	if ( ( rc = register_netdev ( netdev ) ) != 0 )
		goto err_register_netdev;

	return 0;

 err_register_netdev:
 err_mii_reset:
	velocity_reset ( vlc );
 err_reset:
	netdev_nullify ( netdev );
	netdev_put ( netdev );
 err_alloc:
	return rc;
}

/**
 * Remove PCI device
 *
 * @v pci		PCI device
 */
static void velocity_remove ( struct pci_device *pci ) {
	struct net_device *netdev = pci_get_drvdata ( pci );
	struct velocity_nic *vlc = netdev->priv;

	/* Unregister network device */
	unregister_netdev ( netdev );

	/* Reset card */
	velocity_reset ( vlc );

	/* Free network device */
	netdev_nullify ( netdev );
	netdev_put ( netdev );
}

/** Velocity PCI device IDs */
static struct pci_device_id velocity_nics[] = {
	PCI_ROM ( 0x1106, 0x3119, "vt6122",	"VIA Velocity", 0 ),
};

/** Velocity PCI driver */
struct pci_driver velocity_driver __pci_driver = {
	.ids = velocity_nics,
	.id_count = ( sizeof ( velocity_nics ) / sizeof ( velocity_nics[0] ) ),
	.probe = velocity_probe,
	.remove = velocity_remove,
};
