mirror of https://github.com/ipxe/ipxe.git
608 lines
16 KiB
C
608 lines
16 KiB
C
/*
|
|
natsemi.c - gPXE driver for the NatSemi DP8381x series.
|
|
|
|
Based on:
|
|
|
|
natsemi.c: An Etherboot driver for the NatSemi DP8381x series.
|
|
|
|
Copyright (C) 2001 Entity Cyber, Inc.
|
|
|
|
This development of this Etherboot driver was funded by
|
|
|
|
Sicom Systems: http://www.sicompos.com/
|
|
|
|
Author: Marty Connor <mdc@etherboot.org>
|
|
Adapted from a Linux driver which was written by Donald Becker
|
|
|
|
This software may be used and distributed according to the terms
|
|
of the GNU Public License (GPL), incorporated herein by reference.
|
|
|
|
Original Copyright Notice:
|
|
|
|
Written/copyright 1999-2001 by Donald Becker.
|
|
|
|
This software may be used and distributed according to the terms of
|
|
the GNU General Public License (GPL), incorporated herein by reference.
|
|
Drivers based on or derived from this code fall under the GPL and must
|
|
retain the authorship, copyright and license notice. This file is not
|
|
a complete program and may only be used when the entire operating
|
|
system is licensed under the GPL. License for under other terms may be
|
|
available. Contact the original author for details.
|
|
|
|
The original author may be reached as becker@scyld.com, or at
|
|
Scyld Computing Corporation
|
|
410 Severn Ave., Suite 210
|
|
Annapolis MD 21403
|
|
|
|
Support information and updates available at
|
|
http://www.scyld.com/network/netsemi.html
|
|
|
|
References:
|
|
|
|
http://www.scyld.com/expert/100mbps.html
|
|
http://www.scyld.com/expert/NWay.html
|
|
Datasheet is available from:
|
|
http://www.national.com/pf/DP/DP83815.html
|
|
|
|
*/
|
|
|
|
/* Revision History */
|
|
|
|
/*
|
|
02 Jul 2007 Udayan Kumar 1.2 ported the driver from etherboot to gPXE API.
|
|
Fully rewritten,adapting the old driver.
|
|
Added a circular buffer for transmit and receive.
|
|
transmit routine will not wait for transmission to finish.
|
|
poll routine deals with it.
|
|
13 Dec 2003 Tim Legge 1.1 Enabled Multicast Support
|
|
29 May 2001 Marty Connor 1.0 Initial Release. Tested with Netgear FA311 and FA312 boards
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <gpxe/io.h>
|
|
#include <errno.h>
|
|
#include <byteswap.h>
|
|
#include <unistd.h>
|
|
#include <gpxe/pci.h>
|
|
#include <gpxe/if_ether.h>
|
|
#include <gpxe/ethernet.h>
|
|
#include <gpxe/iobuf.h>
|
|
#include <gpxe/netdevice.h>
|
|
#include <gpxe/spi_bit.h>
|
|
#include <gpxe/threewire.h>
|
|
#include <gpxe/nvo.h>
|
|
#include "natsemi.h"
|
|
|
|
/* Function Prototypes: */
|
|
|
|
static int natsemi_spi_read_bit ( struct bit_basher *, unsigned int );
|
|
static void natsemi_spi_write_bit ( struct bit_basher *,unsigned int, unsigned long );
|
|
static void natsemi_init_eeprom ( struct natsemi_private * );
|
|
static int natsemi_probe (struct pci_device *pci, const struct pci_device_id *id);
|
|
static void natsemi_reset (struct net_device *netdev);
|
|
static int natsemi_open (struct net_device *netdev);
|
|
static int natsemi_transmit (struct net_device *netdev, struct io_buffer *iobuf);
|
|
static void natsemi_poll (struct net_device *netdev);
|
|
static void natsemi_close (struct net_device *netdev);
|
|
static void natsemi_irq (struct net_device *netdev, int enable);
|
|
static void natsemi_remove (struct pci_device *pci);
|
|
|
|
/** natsemi net device operations */
|
|
static struct net_device_operations natsemi_operations = {
|
|
.open = natsemi_open,
|
|
.close = natsemi_close,
|
|
.transmit = natsemi_transmit,
|
|
.poll = natsemi_poll,
|
|
.irq = natsemi_irq,
|
|
};
|
|
|
|
static int natsemi_spi_read_bit ( struct bit_basher *basher,
|
|
unsigned int bit_id ) {
|
|
struct natsemi_private *np = container_of ( basher, struct natsemi_private,
|
|
spibit.basher );
|
|
uint8_t mask = natsemi_ee_bits[bit_id];
|
|
uint8_t eereg;
|
|
|
|
eereg = inb ( np->ioaddr + EE_REG );
|
|
return ( eereg & mask );
|
|
}
|
|
|
|
static void natsemi_spi_write_bit ( struct bit_basher *basher,
|
|
unsigned int bit_id, unsigned long data ) {
|
|
struct natsemi_private *np = container_of ( basher, struct natsemi_private,
|
|
spibit.basher );
|
|
uint8_t mask = natsemi_ee_bits[bit_id];
|
|
uint8_t eereg;
|
|
|
|
eereg = inb ( np->ioaddr + EE_REG );
|
|
eereg &= ~mask;
|
|
eereg |= ( data & mask );
|
|
outb ( eereg, np->ioaddr + EE_REG );
|
|
}
|
|
|
|
static struct bit_basher_operations natsemi_basher_ops = {
|
|
.read = natsemi_spi_read_bit,
|
|
.write = natsemi_spi_write_bit,
|
|
};
|
|
|
|
/* It looks that this portion of EEPROM can be used for
|
|
* non-volatile stored options. Data sheet does not talk about this region.
|
|
* Currently it is not working. But with some efforts it can.
|
|
*/
|
|
static struct nvo_fragment natsemi_nvo_fragments[] = {
|
|
{ 0x0c, 0x68 },
|
|
{ 0, 0 }
|
|
};
|
|
|
|
/*
|
|
* Set up for EEPROM access
|
|
*
|
|
* @v NAT NATSEMI NIC
|
|
*/
|
|
static void natsemi_init_eeprom ( struct natsemi_private *np ) {
|
|
|
|
/* Initialise three-wire bus
|
|
*/
|
|
np->spibit.basher.op = &natsemi_basher_ops;
|
|
np->spibit.bus.mode = SPI_MODE_THREEWIRE;
|
|
np->spibit.endianness = SPI_BIT_LITTLE_ENDIAN;
|
|
init_spi_bit_basher ( &np->spibit );
|
|
|
|
/*natsemi DP 83815 only supports at93c46
|
|
*/
|
|
init_at93c46 ( &np->eeprom, 16 );
|
|
np->eeprom.bus = &np->spibit.bus;
|
|
np->nvo.nvs = &np->eeprom.nvs;
|
|
np->nvo.fragments = natsemi_nvo_fragments;
|
|
}
|
|
|
|
/**
|
|
* Probe PCI device
|
|
*
|
|
* @v pci PCI device
|
|
* @v id PCI ID
|
|
* @ret rc Return status code
|
|
*/
|
|
static int natsemi_probe (struct pci_device *pci,
|
|
const struct pci_device_id *id __unused) {
|
|
struct net_device *netdev;
|
|
struct natsemi_private *np = NULL;
|
|
uint8_t ll_addr_encoded[MAX_LL_ADDR_LEN];
|
|
uint8_t last=0,last1=0;
|
|
uint8_t prev_bytes[2];
|
|
int i;
|
|
int rc;
|
|
|
|
/* Allocate net device
|
|
*/
|
|
netdev = alloc_etherdev (sizeof (*np));
|
|
if (! netdev)
|
|
return -ENOMEM;
|
|
|
|
netdev_init (netdev, &natsemi_operations);
|
|
np = netdev->priv;
|
|
pci_set_drvdata (pci, netdev);
|
|
netdev->dev = &pci->dev;
|
|
memset (np, 0, sizeof (*np));
|
|
np->ioaddr = pci->ioaddr;
|
|
|
|
adjust_pci_device (pci);
|
|
|
|
natsemi_reset (netdev);
|
|
natsemi_init_eeprom ( np );
|
|
nvs_read ( &np->eeprom.nvs, EE_MAC-1, prev_bytes, 1 );
|
|
nvs_read ( &np->eeprom.nvs, EE_MAC, ll_addr_encoded, ETH_ALEN );
|
|
|
|
/* decoding the MAC address read from NVS
|
|
* and save it in netdev->ll_addr
|
|
*/
|
|
last = prev_bytes[1] >> 7;
|
|
for ( i = 0 ; i < ETH_ALEN ; i++ ) {
|
|
last1 = ll_addr_encoded[i] >> 7;
|
|
netdev->ll_addr[i] = ll_addr_encoded[i] << 1 | last;
|
|
last = last1;
|
|
}
|
|
|
|
/* Mark as link up; we don't yet handle link state */
|
|
netdev_link_up ( netdev );
|
|
|
|
if ((rc = register_netdev (netdev)) != 0)
|
|
goto err_register_netdev;
|
|
|
|
return 0;
|
|
|
|
err_register_netdev:
|
|
|
|
natsemi_reset (netdev);
|
|
netdev_put (netdev);
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* Remove PCI device
|
|
*
|
|
* @v pci PCI device
|
|
*/
|
|
static void natsemi_remove (struct pci_device *pci) {
|
|
struct net_device *netdev = pci_get_drvdata (pci);
|
|
|
|
unregister_netdev (netdev);
|
|
natsemi_reset (netdev);
|
|
netdev_nullify ( netdev );
|
|
netdev_put (netdev);
|
|
}
|
|
|
|
/**
|
|
* Reset NIC
|
|
*
|
|
* @v NATSEMI NIC
|
|
*
|
|
* Issues a hardware reset and waits for the reset to complete.
|
|
*/
|
|
static void natsemi_reset (struct net_device *netdev)
|
|
{
|
|
struct natsemi_private *np = netdev->priv;
|
|
int i;
|
|
u32 cfg;
|
|
u32 wcsr;
|
|
u32 rfcr;
|
|
u16 pmatch[3];
|
|
u16 sopass[3];
|
|
|
|
natsemi_irq (netdev, 0);
|
|
|
|
/*
|
|
* Resetting the chip causes some registers to be lost.
|
|
* Natsemi suggests NOT reloading the EEPROM while live, so instead
|
|
* we save the state that would have been loaded from EEPROM
|
|
* on a normal power-up (see the spec EEPROM map).
|
|
*/
|
|
|
|
/* CFG */
|
|
cfg = inl (np->ioaddr + ChipConfig) & CFG_RESET_SAVE;
|
|
|
|
/* WCSR */
|
|
wcsr = inl (np->ioaddr + WOLCmd) & WCSR_RESET_SAVE;
|
|
|
|
/* RFCR */
|
|
rfcr = readl (np->ioaddr + RxFilterAddr) & RFCR_RESET_SAVE;
|
|
|
|
/* PMATCH */
|
|
for (i = 0; i < 3; i++) {
|
|
outl(i*2, np->ioaddr + RxFilterAddr);
|
|
pmatch[i] = inw(np->ioaddr + RxFilterData);
|
|
}
|
|
|
|
/* SOPAS */
|
|
for (i = 0; i < 3; i++) {
|
|
outl(0xa+(i*2), np->ioaddr + RxFilterAddr);
|
|
sopass[i] = inw(np->ioaddr + RxFilterData);
|
|
}
|
|
|
|
/* now whack the chip */
|
|
outl(ChipReset, np->ioaddr + ChipCmd);
|
|
for (i=0; i<NATSEMI_HW_TIMEOUT; i++) {
|
|
if (! (inl (np->ioaddr + ChipCmd) & ChipReset))
|
|
break;
|
|
udelay(5);
|
|
}
|
|
if (i == NATSEMI_HW_TIMEOUT) {
|
|
DBG ("natsemi_reset: reset did not complete in %d usec.\n", i*5);
|
|
}
|
|
|
|
/* restore CFG */
|
|
cfg |= inl(np->ioaddr + ChipConfig) & ~CFG_RESET_SAVE;
|
|
cfg &= ~(CfgExtPhy | CfgPhyDis);
|
|
outl (cfg, np->ioaddr + ChipConfig);
|
|
|
|
/* restore WCSR */
|
|
wcsr |= inl (np->ioaddr + WOLCmd) & ~WCSR_RESET_SAVE;
|
|
outl (wcsr, np->ioaddr + WOLCmd);
|
|
|
|
/* read RFCR */
|
|
rfcr |= inl (np->ioaddr + RxFilterAddr) & ~RFCR_RESET_SAVE;
|
|
|
|
/* restore PMATCH */
|
|
for (i = 0; i < 3; i++) {
|
|
outl (i*2, np->ioaddr + RxFilterAddr);
|
|
outw (pmatch[i], np->ioaddr + RxFilterData);
|
|
}
|
|
for (i = 0; i < 3; i++) {
|
|
outl (0xa+(i*2), np->ioaddr + RxFilterAddr);
|
|
outw (sopass[i], np->ioaddr + RxFilterData);
|
|
}
|
|
/* restore RFCR */
|
|
outl (rfcr, np->ioaddr + RxFilterAddr);
|
|
}
|
|
|
|
/**
|
|
* Open NIC
|
|
*
|
|
* @v netdev Net device
|
|
* @ret rc Return status code
|
|
*/
|
|
static int natsemi_open (struct net_device *netdev)
|
|
{
|
|
struct natsemi_private *np = netdev->priv;
|
|
uint32_t tx_config, rx_config;
|
|
int i;
|
|
|
|
/* Disable PME:
|
|
* The PME bit is initialized from the EEPROM contents.
|
|
* PCI cards probably have PME disabled, but motherboard
|
|
* implementations may have PME set to enable WakeOnLan.
|
|
* With PME set the chip will scan incoming packets but
|
|
* nothing will be written to memory.
|
|
*/
|
|
outl (inl (np->ioaddr + ClkRun) & ~0x100, np->ioaddr + ClkRun);
|
|
|
|
/* Set MAC address in NIC
|
|
*/
|
|
for (i = 0 ; i < ETH_ALEN ; i+=2) {
|
|
outl (i, np->ioaddr + RxFilterAddr);
|
|
outw (netdev->ll_addr[i] + (netdev->ll_addr[i + 1] << 8),
|
|
np->ioaddr + RxFilterData);
|
|
}
|
|
|
|
/* Setup Tx Ring
|
|
*/
|
|
np->tx_cur = 0;
|
|
np->tx_dirty = 0;
|
|
for (i = 0 ; i < TX_RING_SIZE ; i++) {
|
|
np->tx[i].link = virt_to_bus ((i + 1 < TX_RING_SIZE) ? &np->tx[i + 1] : &np->tx[0]);
|
|
np->tx[i].cmdsts = 0;
|
|
np->tx[i].bufptr = 0;
|
|
}
|
|
outl (virt_to_bus (&np->tx[0]),np->ioaddr + TxRingPtr);
|
|
|
|
DBG ("Natsemi Tx descriptor loaded with: %#08x\n",
|
|
inl (np->ioaddr + TxRingPtr));
|
|
|
|
/* Setup RX ring
|
|
*/
|
|
np->rx_cur = 0;
|
|
for (i = 0 ; i < NUM_RX_DESC ; i++) {
|
|
np->iobuf[i] = alloc_iob (RX_BUF_SIZE);
|
|
if (! np->iobuf[i])
|
|
goto memory_alloc_err;
|
|
np->rx[i].link = virt_to_bus ((i + 1 < NUM_RX_DESC)
|
|
? &np->rx[i + 1] : &np->rx[0]);
|
|
np->rx[i].cmdsts = RX_BUF_SIZE;
|
|
np->rx[i].bufptr = virt_to_bus (np->iobuf[i]->data);
|
|
DBG (" Address of iobuf [%d] = %p and iobuf->data = %p \n", i,
|
|
&np->iobuf[i], &np->iobuf[i]->data);
|
|
}
|
|
outl (virt_to_bus (&np->rx[0]), np->ioaddr + RxRingPtr);
|
|
|
|
DBG ("Natsemi Rx descriptor loaded with: %#08x\n",
|
|
inl (np->ioaddr + RxRingPtr));
|
|
|
|
/* Setup RX Filter
|
|
*/
|
|
outl (RxFilterEnable | AcceptBroadcast | AcceptAllMulticast | AcceptMyPhys,
|
|
np->ioaddr + RxFilterAddr);
|
|
|
|
/* Initialize other registers.
|
|
* Configure the PCI bus bursts and FIFO thresholds.
|
|
* Configure for standard, in-spec Ethernet.
|
|
*/
|
|
if (inl (np->ioaddr + ChipConfig) & 0x20000000) { /* Full duplex */
|
|
DBG ("Full duplex\n");
|
|
tx_config = 0xD0801002 | 0xC0000000;
|
|
rx_config = 0x10000020 | 0x10000000;
|
|
} else {
|
|
DBG ("Half duplex\n");
|
|
tx_config = 0x10801002 & ~0xC0000000;
|
|
rx_config = 0x00000020 & ~0x10000000;
|
|
}
|
|
outl (tx_config, np->ioaddr + TxConfig);
|
|
outl (rx_config, np->ioaddr + RxConfig);
|
|
|
|
DBG ("Tx config register = %#08x Rx config register = %#08x\n",
|
|
inl (np->ioaddr + TxConfig),
|
|
inl (np->ioaddr + RxConfig));
|
|
|
|
/*Set the Interrupt Mask register
|
|
*/
|
|
outl((RxOk|RxErr|TxOk|TxErr),np->ioaddr + IntrMask);
|
|
/*start the receiver
|
|
*/
|
|
outl (RxOn, np->ioaddr + ChipCmd);
|
|
|
|
return 0;
|
|
|
|
memory_alloc_err:
|
|
|
|
/* Frees any allocated buffers when memory
|
|
* for all buffers requested is not available
|
|
*/
|
|
i = 0;
|
|
while (np->rx[i].cmdsts == RX_BUF_SIZE) {
|
|
free_iob (np->iobuf[i]);
|
|
i++;
|
|
}
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/**
|
|
* Close NIC
|
|
*
|
|
* @v netdev Net device
|
|
*/
|
|
static void natsemi_close (struct net_device *netdev)
|
|
{
|
|
struct natsemi_private *np = netdev->priv;
|
|
int i;
|
|
|
|
natsemi_reset (netdev);
|
|
|
|
for (i = 0; i < NUM_RX_DESC ; i++) {
|
|
free_iob (np->iobuf[i]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transmit packet
|
|
*
|
|
* @v netdev Network device
|
|
* @v iobuf I/O buffer
|
|
* @ret rc Return status code
|
|
*/
|
|
static int natsemi_transmit (struct net_device *netdev, struct io_buffer *iobuf)
|
|
{
|
|
struct natsemi_private *np = netdev->priv;
|
|
|
|
if (np->tx[np->tx_cur].cmdsts != 0) {
|
|
DBG ("TX overflow\n");
|
|
return -ENOBUFS;
|
|
}
|
|
|
|
/* Used by netdev_tx_complete ()
|
|
*/
|
|
np->tx_iobuf[np->tx_cur] = iobuf;
|
|
|
|
/* Pad and align packet has not been used because its not required
|
|
* by the hardware.
|
|
* iob_pad (iobuf, ETH_ZLEN);
|
|
* can be used to achieve it, if required
|
|
*/
|
|
|
|
/* Add the packet to TX ring
|
|
*/
|
|
np->tx[np->tx_cur].bufptr = virt_to_bus (iobuf->data);
|
|
np->tx[np->tx_cur].cmdsts = iob_len (iobuf) | OWN;
|
|
|
|
DBG ("TX id %d at %#08lx + %#08zx\n", np->tx_cur,
|
|
virt_to_bus (&iobuf->data), iob_len (iobuf));
|
|
|
|
/* increment the circular buffer pointer to the next buffer location
|
|
*/
|
|
np->tx_cur = (np->tx_cur + 1) % TX_RING_SIZE;
|
|
|
|
/*start the transmitter
|
|
*/
|
|
outl (TxOn, np->ioaddr + ChipCmd);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Poll for received packets
|
|
*
|
|
* @v netdev Network device
|
|
*/
|
|
static void natsemi_poll (struct net_device *netdev)
|
|
{
|
|
struct natsemi_private *np = netdev->priv;
|
|
unsigned int tx_status;
|
|
unsigned int rx_status;
|
|
unsigned int intr_status;
|
|
unsigned int rx_len;
|
|
struct io_buffer *rx_iob;
|
|
int i;
|
|
|
|
/* read the interrupt register
|
|
*/
|
|
intr_status = inl (np->ioaddr + IntrStatus);
|
|
|
|
if (!intr_status)
|
|
goto end;
|
|
|
|
DBG ("natsemi_poll: intr_status = %#08x\n", intr_status);
|
|
|
|
/* Check status of transmitted packets
|
|
*/
|
|
i = np->tx_dirty;
|
|
while (i != np->tx_cur) {
|
|
tx_status = np->tx[np->tx_dirty].cmdsts;
|
|
|
|
DBG ("tx_dirty = %d tx_cur=%d tx_status=%#08x\n",
|
|
np->tx_dirty, np->tx_cur, tx_status);
|
|
|
|
if (tx_status & OWN)
|
|
break;
|
|
|
|
if (! (tx_status & DescPktOK)) {
|
|
netdev_tx_complete_err (netdev,np->tx_iobuf[np->tx_dirty],-EINVAL);
|
|
DBG ("Error transmitting packet, tx_status: %#08x\n",
|
|
tx_status);
|
|
} else {
|
|
netdev_tx_complete (netdev, np->tx_iobuf[np->tx_dirty]);
|
|
DBG ("Success transmitting packet\n");
|
|
}
|
|
|
|
np->tx[np->tx_dirty].cmdsts = 0;
|
|
np->tx_dirty = (np->tx_dirty + 1) % TX_RING_SIZE;
|
|
i = (i + 1) % TX_RING_SIZE;
|
|
}
|
|
|
|
/* Process received packets
|
|
*/
|
|
rx_status = (unsigned int) np->rx[np->rx_cur].cmdsts;
|
|
while ((rx_status & OWN)) {
|
|
rx_len = (rx_status & DSIZE) - CRC_SIZE;
|
|
|
|
DBG ("Received packet, rx_curr = %d, rx_status = %#08x, rx_len = %d\n",
|
|
np->rx_cur, rx_status, rx_len);
|
|
|
|
if ((rx_status & (DescMore | DescPktOK | RxTooLong)) != DescPktOK) {
|
|
netdev_rx_err (netdev, NULL, -EINVAL);
|
|
|
|
DBG ("natsemi_poll: Corrupted packet received!"
|
|
" Status = %#08x\n",
|
|
np->rx[np->rx_cur].cmdsts);
|
|
|
|
} else {
|
|
|
|
|
|
/* If unable allocate space for this packet,
|
|
* try again next poll
|
|
*/
|
|
rx_iob = alloc_iob (rx_len);
|
|
if (! rx_iob)
|
|
goto end;
|
|
memcpy (iob_put (rx_iob, rx_len),
|
|
np->iobuf[np->rx_cur]->data, rx_len);
|
|
/* Add this packet to the receive queue.
|
|
*/
|
|
netdev_rx (netdev, rx_iob);
|
|
}
|
|
np->rx[np->rx_cur].cmdsts = RX_BUF_SIZE;
|
|
np->rx_cur = (np->rx_cur + 1) % NUM_RX_DESC;
|
|
rx_status = np->rx[np->rx_cur].cmdsts;
|
|
}
|
|
end:
|
|
/* re-enable the potentially idle receive state machine
|
|
*/
|
|
outl (RxOn, np->ioaddr + ChipCmd);
|
|
}
|
|
|
|
/**
|
|
* Enable/disable interrupts
|
|
*
|
|
* @v netdev Network device
|
|
* @v enable Non-zero for enable, zero for disable
|
|
*/
|
|
static void natsemi_irq (struct net_device *netdev, int enable)
|
|
{
|
|
struct natsemi_private *np = netdev->priv;
|
|
|
|
outl ((enable ? (RxOk | RxErr | TxOk|TxErr) : 0),
|
|
np->ioaddr + IntrMask);
|
|
outl ((enable ? 1 : 0), np->ioaddr + IntrEnable);
|
|
}
|
|
|
|
static struct pci_device_id natsemi_nics[] = {
|
|
PCI_ROM(0x100b, 0x0020, "dp83815", "DP83815"),
|
|
};
|
|
|
|
struct pci_driver natsemi_driver __pci_driver = {
|
|
.ids = natsemi_nics,
|
|
.id_count = (sizeof (natsemi_nics) / sizeof (natsemi_nics[0])),
|
|
.probe = natsemi_probe,
|
|
.remove = natsemi_remove,
|
|
};
|