Goto sanos source index

//
// 3c905c.c
//
// 3Com 3C905C NIC network driver
//
// Copyright (C) 2002 Michael Ringgaard. All rights reserved.
// Copyright (C) 1999 3Com Corporation. All rights reserved.
// 
// 3Com Network Driver software is distributed as is, without any warranty
// of any kind, either express or implied as further specified in the GNU Public
// License. This software may be used and distributed according to the terms of
// the GNU Public License, located in the file LICENSE.
//
// 3Com and EtherLink are registered trademarks of 3Com Corporation.
// 

#include <os/krnl.h>
#include "3c905c.h"

#define tracenic 0
#define nictrace if (tracenic) kprintf

char *connectorname[] = {"10Base-T", "AUI", "n/a", "BNC", "100Base-TX", "100Base-FX", "MII", "n/a", "Auto", "Ext-MII"};

struct sg_entry {
  unsigned long addr;
  unsigned long length;
};

struct rx_desc {
  unsigned long phys_next;
  unsigned long status;
  struct sg_entry sglist[1];
  struct rx_desc *next;
  struct pbuf *pkt;
  char filler[8];
};

struct tx_desc {
  unsigned long phys_next;
  unsigned long header;
  struct sg_entry sglist[TX_MAX_FRAGS];
  struct tx_desc *next;
  struct tx_desc *prev;
  struct pbuf *pkt;
  unsigned long phys_addr;
  char filler[8];
};

struct nicstat {
  // Transmit statistics.
  unsigned long tx_frames_ok;
  unsigned long tx_bytes_ok;
  unsigned long tx_frames_deferred;
  unsigned long tx_single_collisions;
  unsigned long tx_multiple_collisions;
  unsigned long tx_late_collisions;
  unsigned long tx_carrier_lost;

  unsigned long tx_maximum_collisions;
  unsigned long tx_sqe_errors;
  unsigned long tx_hw_errors;
  unsigned long tx_jabber_error;
  unsigned long tx_unknown_error;

  // Receive statistics.
  unsigned long rx_frames_ok;
  unsigned long rx_bytes_ok;

  unsigned long rx_overruns;
  unsigned long rx_bad_ssd;
  unsigned long rx_alignment_error;
  unsigned long rx_bad_crc_error;
  unsigned long rx_oversize_error;
};

struct nic {
  struct rx_desc rx_ring[RX_RING_SIZE];
  struct tx_desc tx_ring[TX_RING_SIZE];
  struct rx_desc *curr_rx;              // Next entry to receive from rx ring
  struct tx_desc *curr_tx;              // Next entry to reclaim from tx ring
  struct tx_desc *next_tx;              // Next entry to add in tx ring

  struct sem tx_sem;                    // Semaphore for tx ring
  int tx_size;                          // Number of active entries in transmit list

  dev_t devno;                          // Device number
  unsigned long deviceid;               // PCI device id

  unsigned short iobase;                // Configured I/O base
  unsigned short irq;                   // Configured IRQ

  int autoselect;                       // Auto-negotiate
  int connector;                        // Active connector
  int linkspeed;                        // Link speed in mbits/s
  int fullduplex;                       // Full duplex link
  int flowcontrol;                      // Flow control

  struct eth_addr hwaddr;               // MAC address for NIC

  struct interrupt intr;                // Interrupt object for driver
  struct dpc dpc;                       // DPC for driver

  struct nicstat stat;                  // NIC statistics
  unsigned short eeprom[EEPROM_SIZE];   // EEPROM contents
};

struct netstats *netstats;

void clear_statistics(struct nic *nic);
void update_statistics(struct nic *nic);
void update_statistics(struct nic *nic);
int nicstat_proc(struct proc_file *pf, void *arg);

int nic_find_mii_phy(struct nic *nic);
void nic_send_mii_phy_preamble(struct nic *nic);
void nic_write_mii_phy(struct nic *nic, unsigned short reg, unsigned short value);
int nic_read_mii_phy(struct nic *nic, unsigned short reg);

int nic_eeprom_busy(struct nic *nic);
int nic_read_eeprom(struct nic *nic, unsigned short addr);

int nic_transmit(struct dev *dev, struct pbuf *p);
int nic_ioctl(struct dev *dev, int cmd, void *args, size_t size);
int nic_attach(struct dev *dev, struct eth_addr *hwaddr);
int nic_detach(struct dev *dev);
void nic_up_complete(struct nic *nic);
void nic_down_complete(struct nic *nic);
void nic_host_error(struct nic *nic);
void nic_tx_complete(struct nic *nic);
void nic_dpc(void *arg);
int nic_handler(struct context *ctxt, void *arg);

int nic_try_mii(struct nic *nic, unsigned short options);
int nic_negotiate_link(struct nic *nic, unsigned short options);
int nic_program_mii(struct nic *nic, int connector);
int nic_initialize_adapter(struct nic *nic);
int nic_get_link_speed(struct nic *nic);
int nic_restart_receiver(struct nic *nic);
int nic_restart_transmitter(struct nic *nic);
int nic_flow_control(struct nic *nic);
int nic_configure_mii(struct nic *nic, unsigned short media_options);
int nic_check_mii_configuration(struct nic *nic, unsigned short media_options);
int nic_check_mii_auto_negotiation_status(struct nic *nic);
int nic_check_dc_converter(struct nic *nic, int enabled);
int nic_setup_connector(struct nic *nic, int connector);
int nic_setup_media(struct nic *nic);
int nic_setup_buffers(struct nic *nic);

struct driver nic_driver = {
  "3c905c",
  DEV_TYPE_PACKET,
  nic_ioctl,
  NULL,
  NULL,
  nic_attach,
  nic_detach,
  nic_transmit
};

__inline void execute_command(struct nic *nic, int cmd, int param) {
  outpw(nic->iobase + CMD, (unsigned short) (cmd | param));
}

int execute_command_wait(struct nic *nic, int cmd, int param) {
  int i;

  execute_command(nic, cmd, param);
  for (i = 0; i < 100000; i++) {
    if (!(inpw(nic->iobase + STATUS) & INTSTATUS_CMD_IN_PROGRESS)) return 0;
    udelay(10);
  }

  for (i = 0; i < 200; i++) {
    if (!(inpw(nic->iobase + STATUS) & INTSTATUS_CMD_IN_PROGRESS)) return 0;
    msleep(10);
  }

  kprintf(KERN_ERR "nic: command did not complete\n");
  return -ETIMEOUT;
}

__inline void select_window(struct nic *nic, int window) {
  execute_command(nic, CMD_SELECT_WINDOW, window);
}

void clear_statistics(struct nic *nic) {
  int i;

  select_window(nic, 6);
  for (i = FIRST_BYTE_STAT; i <= LAST_BYTE_STAT; i++) inp(nic->iobase + i);
  inpw(nic->iobase + BYTES_RECEIVED_OK);
  inpw(nic->iobase + BYTES_XMITTED_OK);
  select_window(nic, 4);
  inp(nic->iobase + BAD_SSD);
  inp(nic->iobase + UPPER_BYTES_OK);
}

void update_statistics(struct nic *nic) {
  int current_window;
  unsigned long rx_frames;
  unsigned long tx_frames;
  unsigned long rx_bytes;
  unsigned long tx_bytes;
  unsigned char upper;
  
  // Read the current window
  current_window = inpw(nic->iobase + STATUS) >> 13;

  // Read statistics from window 6
  select_window(nic, 6);

  nic->stat.tx_sqe_errors += inp(nic->iobase + SQE_ERRORS) & 0xFF;
  nic->stat.tx_multiple_collisions += inp(nic->iobase + MULTIPLE_COLLISIONS) & 0xFF;
  nic->stat.tx_single_collisions += inp(nic->iobase + SINGLE_COLLISIONS) & 0xFF;
  nic->stat.rx_overruns += inp(nic->iobase + RX_OVERRUNS) & 0xFF;
  nic->stat.tx_carrier_lost += inp(nic->iobase + CARRIER_LOST) & 0xFF;
  nic->stat.tx_late_collisions += inp(nic->iobase + LATE_COLLISIONS) & 0xFF;
  nic->stat.tx_frames_deferred += inp(nic->iobase + FRAMES_DEFERRED) & 0xFF;

  // Frames received/transmitted
  rx_frames = inp(nic->iobase + FRAMES_RECEIVED_OK) & 0xFF;
  tx_frames = inp(nic->iobase + FRAMES_XMITTED_OK) & 0xFF;
  upper = inp(nic->iobase + UPPER_FRAMES_OK) & 0xFF;
  rx_frames += (upper & 0x0F) << 8;
  tx_frames += (upper & 0xF0) << 4;

  nic->stat.rx_frames_ok += rx_frames;
  nic->stat.tx_frames_ok += tx_frames;

  // Bytes received/transmitted - upper part added below from window 4
  rx_bytes = inpw(nic->iobase + BYTES_RECEIVED_OK) & 0xFFFF;
  tx_bytes = inpw(nic->iobase + BYTES_XMITTED_OK) & 0xFFFF;

  // Read final statistics from window 4
  select_window(nic, 4);

  // Update bytes received/transmitted with upper part
  upper = inp(nic->iobase + UPPER_BYTES_OK);
  rx_bytes += (upper & 0x0F) << 16;
  tx_bytes += (upper & 0xF0) << 12;

  nic->stat.rx_bytes_ok += rx_bytes;
  nic->stat.tx_bytes_ok += tx_bytes;

  nic->stat.rx_bad_ssd += inp(nic->iobase + BAD_SSD) & 0xFF;

  // Set the window to its previous value
  select_window(nic, current_window);
}

int nicstat_proc(struct proc_file *pf, void *arg) {
  struct nic *nic = arg;

  update_statistics(nic);

  pprintf(pf, "Frames transmitted... : %lu\n", nic->stat.tx_frames_ok);
  pprintf(pf, "Bytes transmitted.... : %lu\n", nic->stat.tx_bytes_ok);
  pprintf(pf, "Frames deferred...... : %lu\n", nic->stat.tx_frames_deferred);
  pprintf(pf, "Single collisions.... : %lu\n", nic->stat.tx_single_collisions);
  pprintf(pf, "Multiple collisions.. : %lu\n", nic->stat.tx_multiple_collisions);
  pprintf(pf, "Late collisions...... : %lu\n", nic->stat.tx_late_collisions);
  pprintf(pf, "Carrier lost......... : %lu\n", nic->stat.tx_carrier_lost);
  pprintf(pf, "Maximum collisions .. : %lu\n", nic->stat.tx_maximum_collisions);
  pprintf(pf, "SQE errors........... : %lu\n", nic->stat.tx_sqe_errors);
  pprintf(pf, "HW errors............ : %lu\n", nic->stat.tx_hw_errors);
  pprintf(pf, "Jabber errors........ : %lu\n", nic->stat.tx_jabber_error);
  pprintf(pf, "Unknown errors....... : %lu\n", nic->stat.tx_unknown_error);
  pprintf(pf, "Frames received...... : %lu\n", nic->stat.rx_frames_ok);
  pprintf(pf, "Bytes received....... : %lu\n", nic->stat.rx_bytes_ok);
  pprintf(pf, "Overruns............. : %lu\n", nic->stat.rx_overruns);
  pprintf(pf, "Bad SSD.............. : %lu\n", nic->stat.rx_bad_ssd);
  pprintf(pf, "Allignment errors.... : %lu\n", nic->stat.rx_alignment_error);
  pprintf(pf, "CRC errors........... : %lu\n", nic->stat.rx_bad_crc_error);
  pprintf(pf, "Oversized frames..... : %lu\n", nic->stat.rx_oversize_error);

  return 0;
}

int nic_find_mii_phy(struct nic *nic) {
  unsigned short media_options;
  unsigned short phy_management;
  int i;

  // Read the MEDIA OPTIONS to see what connectors are available
  select_window(nic, 3);
  media_options = inpw(nic->iobase + MEDIA_OPTIONS);

  if ((media_options & MEDIA_OPTIONS_MII_AVAILABLE) ||
      (media_options & MEDIA_OPTIONS_100BASET4_AVAILABLE)) {
    // Drop everything, so we are not driving the data, and run the
    // clock through 32 cycles in case the PHY is trying to tell us
    // something. Then read the data line, since the PHY's pull-up
    // will read as a 1 if it's present.
    select_window(nic, 4);
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);

    for (i = 0; i < 32; i++) {
      udelay(1);
      outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_CLOCK);

      udelay(1);
      outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);

    }

    phy_management = inpw(nic->iobase + PHYSICAL_MANAGEMENT);

    if (phy_management & PHY_DATA1) {
      return 0;
    } else {
      return -EIO;
    }
  }

  return 0;
}

void nic_send_mii_phy_preamble(struct nic *nic) {
  int i;

  // Set up and send the preamble, a sequence of 32 "1" bits
  select_window(nic, 4);
  outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE);

  for (i = 0; i < 32; i++) {
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_DATA1);
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_DATA1 | PHY_CLOCK);
    udelay(1);
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE);
    udelay(1);
  }
}

void nic_write_mii_phy(struct nic *nic, unsigned short reg, unsigned short value) {
  int i,j;
  unsigned short writecmd[2];

  writecmd[0] = MII_PHY_ADDRESS_WRITE;
  writecmd[1] = 0;

  nic_send_mii_phy_preamble(nic);

  // Bits 2..6 of the command word specify the register
  writecmd[0] |= (reg & 0x1F) << 2;
  writecmd[1] = value;

  select_window(nic, 4);

  for (i = 0; i < 2; i++) {
    for (j = 0x8000; j; j >>= 1) {
      if (writecmd[i] & j) {
        outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_DATA1);
        outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_DATA1 | PHY_CLOCK);
      } else  {
        outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE);
        outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_CLOCK);
      }
      udelay(1);
      outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE);
      udelay(1);
    }
  }

  // Now give it a couple of clocks with nobody driving
  outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);
  for (i = 0; i < 2; i++) {
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_CLOCK);
    udelay(1);
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);
    udelay(1);
  }
}

int nic_read_mii_phy(struct nic *nic, unsigned short reg) {
  unsigned short  phy_mgmt = 0;
  unsigned short read_cmd;
  unsigned short value;
  int i;

  read_cmd = MII_PHY_ADDRESS_READ;

  nic_send_mii_phy_preamble(nic);
  
  // Bits 2..6 of the command word specify the register
  read_cmd |= (reg & 0x1F) << 2;

  select_window(nic, 4);

  for (i = 0x8000; i > 2; i >>= 1) {
    if (read_cmd & i) {
      outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_DATA1);
      outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_DATA1 | PHY_CLOCK);
    } else {
      outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE);
      outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE | PHY_CLOCK);
    }

    udelay(1);
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_WRITE);
    udelay(1);
  }

  // Now run one clock with nobody driving
  outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);
  outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_CLOCK);
  udelay(1);
  outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);
  udelay(1);

  // Now run one clock, expecting the PHY to be driving a 0 on the data
  // line.  If we read a 1, it has to be just his pull-up, and he's not
  // responding.

  phy_mgmt = inpw(nic->iobase + PHYSICAL_MANAGEMENT);
  if (phy_mgmt & PHY_DATA1) return -EIO;

  // We think we are in sync.  Now we read 16 bits of data from the PHY.
  select_window(nic, 4);
  value = 0;
  for (i = 0x8000; i; i >>= 1) {
    // Shift input up one to make room
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_CLOCK);
    udelay(1);
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);
    udelay(1);

    phy_mgmt = inpw(nic->iobase + PHYSICAL_MANAGEMENT);

    if (phy_mgmt & PHY_DATA1) value |= i;
  }

  // Now give it a couple of clocks with nobody driving
  outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);
  for (i = 0; i < 2; i++) {
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, PHY_CLOCK);
    udelay(1);
    outpw(nic->iobase + PHYSICAL_MANAGEMENT, 0);
    udelay(1);
  }

  return value;
}

int nic_eeprom_busy(struct nic *nic) {
  unsigned short status;
  unsigned long timeout;

  timeout = get_ticks() + 1 * TICKS_PER_SEC;
  while (1) {
    status = inpw(nic->iobase + EEPROM_CMD);
    if (!(status & EEPROM_BUSY)) return 0;
    if (time_after(get_ticks(), timeout)) {
      kprintf(KERN_ERR "nic: timeout reading eeprom\n");
      return -ETIMEOUT;
    }

    udelay(10);
  }
}

int nic_read_eeprom(struct nic *nic, unsigned short addr) {
  if (addr > 0x003F)  addr = (addr & 0x003F) | ((addr & 0x03C0) << 2);

  select_window(nic, 0);

  // Check for eeprom busy
  if (nic_eeprom_busy(nic) < 0) return -ETIMEOUT;

  // Issue the read eeprom data command
  outpw(nic->iobase + EEPROM_CMD, (unsigned short) (EEPROM_CMD_READ + addr));

  // Check for eeprom busy
  if (nic_eeprom_busy(nic) < 0) return -ETIMEOUT;

  // Return value read from eeprom
  return inpw(nic->iobase + EEPROM_DATA);
}

int nic_transmit(struct dev *dev, struct pbuf *p) {
  struct nic *nic = dev->privdata;
  struct pbuf *q;
  unsigned long dnlistptr;
  int i;
  struct tx_desc *entry;

  nictrace("nic: transmit packet, %d bytes, %d fragments\n", p->tot_len, pbuf_clen(p));

  // Wait for free entry in transmit ring
  if (wait_for_object(&nic->tx_sem, TX_TIMEOUT) < 0) {
    kprintf(KERN_ERR "nic: transmit timeout, drop packet\n");
    netstats->link.drop++;
    return -ETIMEOUT;
  }

  // Check for over-fragmented packets
  if (pbuf_clen(p) > TX_MAX_FRAGS) {
    p = pbuf_linearize(PBUF_RAW, p);
    netstats->link.memerr++;
    netstats->link.drop++;
    if (!p) return -ENOMEM;
  }

  // Fill tx entry
  entry = nic->next_tx;
  for (q = p, i = 0; q != NULL; q = q->next, i++) {
    entry->sglist[i].addr = virt2phys(q->payload);
    if (!q->next) {
      entry->sglist[i].length = q->len | LAST_FRAG;
    } else {
      entry->sglist[i].length = q->len;
    }
  }

  entry->header = FSH_ROUND_UP_DEFEAT | FSH_DOWN_INDICATE | FSH_TX_INDICATE | FSH_ADD_IP_CHECKSUM | FSH_ADD_TCP_CHECKSUM | FSH_ADD_UDP_CHECKSUM;
  entry->phys_next = 0;
  entry->pkt = p;

  // Move to next entry in tx_ring
  nic->next_tx = entry->next;
  nic->tx_size++;

  // Update download list
  execute_command_wait(nic, CMD_DOWN_STALL, 0);
  dnlistptr = inpd(nic->iobase + DOWN_LIST_POINTER);
  if (dnlistptr == 0) {
    nictrace("nic: set dnlist to %p\n", entry->phys_addr);
    outpd(nic->iobase + DOWN_LIST_POINTER, entry->phys_addr);
  } else {
    nictrace("nic: chaining %p to %p\n", entry->phys_addr, entry->prev->phys_next);
    entry->prev->phys_next = entry->phys_addr;
  }
  execute_command(nic, CMD_DOWN_UNSTALL, 0);

  netstats->link.xmit++;
  return 0;
}

int nic_ioctl(struct dev *dev, int cmd, void *args, size_t size) {
  return -ENOSYS;
}

int nic_attach(struct dev *dev, struct eth_addr *hwaddr) {
  struct nic *nic = dev->privdata;
  *hwaddr = nic->hwaddr;

  dev->netif->flags |= NETIF_IP_TX_CHECKSUM_OFFLOAD | NETIF_IP_RX_CHECKSUM_OFFLOAD;
  dev->netif->flags |= NETIF_UDP_RX_CHECKSUM_OFFLOAD | NETIF_UDP_TX_CHECKSUM_OFFLOAD;
  dev->netif->flags |= NETIF_TCP_RX_CHECKSUM_OFFLOAD | NETIF_TCP_TX_CHECKSUM_OFFLOAD;

  return 0;
}

int nic_detach(struct dev *dev) {
  return 0;
}

void nic_up_complete(struct nic *nic) {
  unsigned long status;
  int length;
  struct pbuf *p, *q;

  while ((status = nic->curr_rx->status) & UP_COMPLETE) {
    length = nic->curr_rx->status & 0x1FFF;

    nictrace("nic: packet received, %d bytes\n", length);

    // Check for errors
    if (status & UP_PACKET_STATUS_ERROR) {
      netstats->link.err++;

      if (status & UP_PACKET_STATUS_RUNT_FRAME) {
        kprintf(KERN_WARNING "nic: runt frame\n");
      }
      if (status & UP_PACKET_STATUS_ALIGNMENT_ERROR) {
        kprintf(KERN_WARNING "nic: alignment error\n");
        nic->stat.rx_alignment_error++;
      }
      if (status & UP_PACKET_STATUS_CRC_ERROR) {
        kprintf(KERN_WARNING "nic: crc error\n");
        nic->stat.rx_bad_crc_error++;
      }
      if (status & UP_PACKET_STATUS_OVERSIZE_FRAME) {
        kprintf(KERN_WARNING "nic: oversize frame\n");
        nic->stat.rx_oversize_error++;
      }

      nic->curr_rx->status = 0;
      nic->curr_rx = nic->curr_rx->next;
      continue;
    }

    // Check for IP checksum error
    if ((nic->curr_rx->status & UP_PACKET_STATUS_IP_CHECKSUM_CHECKED) &&
        (nic->curr_rx->status & UP_PACKET_STATUS_IP_CHECKSUM_ERROR)) {
      //kprintf("nic: ip checksum error\n");
      netstats->ip.chkerr++;
      nic->curr_rx->status = 0;
      nic->curr_rx = nic->curr_rx->next;
      continue;
    }

    // Check for UDP checksum error
    if ((nic->curr_rx->status & UP_PACKET_STATUS_UDP_CHECKSUM_CHECKED) &&
        (nic->curr_rx->status & UP_PACKET_STATUS_UDP_CHECKSUM_ERROR)) {
      //kprintf("nic: udp checksum error\n");
      netstats->udp.chkerr++;
      nic->curr_rx->status = 0;
      nic->curr_rx = nic->curr_rx->next;
      continue;
    }

    // Check for TCP checksum error
    if ((nic->curr_rx->status & UP_PACKET_STATUS_TCP_CHECKSUM_CHECKED) &&
        (nic->curr_rx->status & UP_PACKET_STATUS_TCP_CHECKSUM_ERROR)) {
      //kprintf("nic: tcp checksum error\n");
      netstats->tcp.chkerr++;
      nic->curr_rx->status = 0;
      nic->curr_rx = nic->curr_rx->next;
      continue;
    }

    if (length < RX_COPYBREAK) {
      // Allocate properly sized pbuf and copy
      p = pbuf_alloc(PBUF_RAW, length, PBUF_RW);
      if (p) {
        memcpy(p->payload, nic->curr_rx->pkt->payload, length);
      } else {
        netstats->link.memerr++;
        netstats->link.drop++;
      }
    } else {
      // Allocate new full sized pbuf and replace existing pbuf in rx ring
      q = pbuf_alloc(PBUF_RAW, ETHER_FRAME_LEN, PBUF_RW);
      if (q) {
        p = nic->curr_rx->pkt;
        pbuf_realloc(p, length);

        nic->curr_rx->pkt = q;
        nic->curr_rx->sglist[0].addr = virt2phys(q->payload);
      } else {
        p = NULL;
        netstats->link.memerr++;
        netstats->link.drop++;
      }
    }

    // Send packet to upper layer
    if (p) {
      netstats->link.recv++;
      if (dev_receive(nic->devno, p) < 0) pbuf_free(p);
    }

    // Clear status and move to next packet in rx ring
    nic->curr_rx->status = 0;
    nic->curr_rx = nic->curr_rx->next;
  }

  // Unstall upload engine
  execute_command(nic, CMD_UP_UNSTALL, 0);
}

void nic_down_complete(struct nic *nic) {
  int freed = 0;

  while (nic->tx_size > 0 && (nic->curr_tx->header & FSH_DOWN_COMPLETE)) {
    if (nic->curr_tx->pkt) {
      pbuf_free(nic->curr_tx->pkt);
      nic->curr_tx->pkt = NULL;
    }
    nic->curr_tx->prev->phys_next = 0;

    freed++;
    nic->tx_size--;
    nic->curr_tx = nic->curr_tx->next;
  }

  nictrace("nic: release %d tx entries\n", freed);

  release_sem(&nic->tx_sem, freed);
}

void nic_host_error(struct nic *nic) {
  execute_command_wait(nic, CMD_RESET, 
    GLOBAL_RESET_MASK_NETWORK_RESET |
    GLOBAL_RESET_MASK_TP_AUI_RESET |
    GLOBAL_RESET_MASK_ENDEC_RESET |
    GLOBAL_RESET_MASK_AISM_RESET |
    GLOBAL_RESET_MASK_SMB_RESET |
    GLOBAL_RESET_MASK_VCO_RESET |
    GLOBAL_RESET_MASK_UP_DOWN_RESET);
}

void nic_tx_complete(struct nic *nic) {
  unsigned char txstatus;

  txstatus = inp(nic->iobase + TX_STATUS);
  outp(nic->iobase + TX_STATUS, txstatus);

  if (txstatus & TX_STATUS_HWERROR) {
    kprintf(KERN_ERR "nic: hw error\n");
    nic->stat.tx_hw_errors++;
    nic_restart_transmitter(nic);
  } else if (txstatus & TX_STATUS_JABBER) {
    kprintf(KERN_ERR "nic: jabber error\n");
    nic->stat.tx_jabber_error++;
    nic_restart_transmitter(nic);
  } else if (txstatus & TX_STATUS_MAXIMUM_COLLISION) {
    kprintf(KERN_ERR "nic: maximum collisions\n");
    nic->stat.tx_maximum_collisions++;
    execute_command(nic, CMD_TX_ENABLE, 0);
  } else if (txstatus & 0x3F) {
    kprintf(KERN_ERR "nic: unknown errror in 0x%02X tx complete\n", txstatus);
    nic->stat.tx_unknown_error++;
    execute_command(nic, CMD_TX_ENABLE, 0);
  }
}

void nic_dpc(void *arg) {
  struct nic *nic = (struct nic *) arg;
  unsigned short status;

  // Read the status
  status = inpw(nic->iobase + STATUS);

  // Return if no interrupt - caused by shared interrupt
  if (!(status & INTSTATUS_INT_LATCH)) return;

  while (1) {
    status &= ALL_INTERRUPTS;
    if (!status) break;

    // Handle host error event
    if (status & INTSTATUS_HOST_ERROR) {
      kprintf(KERN_ERR "nic: host error\n");
      nic_host_error(nic);
    }

    // Handle tx complete event
    if (status & INTSTATUS_TX_COMPLETE) {
      nic_tx_complete(nic);
    }

    // Handle rx complete event
    if (status & INTSTATUS_RX_COMPLETE) {
      kprintf("nic: rx complete\n");
    }

    // Handle rx early event
    if (status & INTSTATUS_RX_EARLY) {
      kprintf("nic: rx early\n");
      execute_command(nic, CMD_ACKNOWLEDGE_INTERRUPT, RX_EARLY_ACK);
    }

    // Handle int requested event
    if (status & INTSTATUS_INT_REQUESTED) {
      kprintf("nic: int request\n");
      execute_command(nic, CMD_ACKNOWLEDGE_INTERRUPT, INT_REQUESTED_ACK);
    }

    // Handle update statistics event
    if (status & INTSTATUS_UPDATE_STATS) {
      update_statistics(nic);
    }

    // Handle link event
    if (status & INTSTATUS_LINK_EVENT) {
      kprintf("nic: link event\n");
    }

    // Handle download complete event
    if (status & INTSTATUS_DN_COMPLETE) {
      nic_down_complete(nic);
      execute_command(nic, CMD_ACKNOWLEDGE_INTERRUPT, DN_COMPLETE_ACK);
    }

    // Handle upload complete event
    if (status & INTSTATUS_UP_COMPLETE) {
      nic_up_complete(nic);
      execute_command(nic, CMD_ACKNOWLEDGE_INTERRUPT, UP_COMPLETE_ACK);
    }

    // Acknowledge the interrupt
    execute_command(nic, CMD_ACKNOWLEDGE_INTERRUPT, INTERRUPT_LATCH_ACK);

    // Get next status
    status = inpw(nic->iobase + STATUS);
  }

  eoi(nic->irq);
}

int nic_handler(struct context *ctxt, void *arg) {
  struct nic *nic = (struct nic *) arg;

  // Queue DPC to service interrupt
  queue_irq_dpc(&nic->dpc, nic_dpc, nic);

  return 0;
}

int nic_try_mii(struct nic *nic, unsigned short options) {
  int phy_status;

  nictrace("enter nic_try_mii\n");

  // First see if there's anything connected to the MII
  if (nic_find_mii_phy(nic) < 0) return -ENODEV;

  // Now we can read the status and try to figure out what's out there.
  phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
  if (phy_status < 0) return phy_status;

  if ((phy_status & MII_STATUS_AUTO) && (phy_status & MII_STATUS_EXTENDED)) {
    // If it is capable of auto negotiation, see if it has been done already.
    // Check the current MII auto-negotiation state and see if we need to
    // start auto-neg over.
 
    if (nic_check_mii_configuration(nic, options) < 0) return -ENODEV;

    // See if link is up...
    phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
    if (phy_status < 0) return phy_status;

    if (phy_status & MII_STATUS_LINK_UP) {
      if (nic_get_link_speed(nic) < 0) return -ENODEV;
      return 0;
    } else  {
      if (phy_status & MII_STATUS_100MB_MASK) {
        nic->linkspeed = 100;
      } else  {
        nic->linkspeed = 10;
      }

      return 0;
    }
  }

  return -ENODEV;
}

int nic_negotiate_link(struct nic *nic, unsigned short options) {
  nictrace("enter nic_negotiate_link\n");
  nic->connector = CONNECTOR_UNKNOWN;

  // Try 100MB Connectors
  if ((options & MEDIA_OPTIONS_100BASETX_AVAILABLE) ||
      (options & MEDIA_OPTIONS_10BASET_AVAILABLE) ||
      (options & MEDIA_OPTIONS_MII_AVAILABLE)) {
    // For 10Base-T and 100Base-TX, select autonegotiation instead of autoselect before calling trymii
    if ((options & MEDIA_OPTIONS_100BASETX_AVAILABLE) || (options & MEDIA_OPTIONS_10BASET_AVAILABLE)) {
      nic->connector = CONNECTOR_AUTONEGOTIATION;
    } else {
      nic->connector = CONNECTOR_MII;
    }

    if (nic_try_mii(nic, options) < 0) nic->connector = CONNECTOR_UNKNOWN;
  }

  // Transceiver available is 100Base-FX
  if ((options & MEDIA_OPTIONS_100BASEFX_AVAILABLE) && nic->connector == CONNECTOR_UNKNOWN) {
    //TODO: try linkbeat
    nic->connector = CONNECTOR_100BASEFX;
    nic->linkspeed = 100;
  }

  // Transceiver available is 10AUI
  if ((options & MEDIA_OPTIONS_10AUI_AVAILABLE) && nic->connector == CONNECTOR_UNKNOWN) {
    nic_setup_connector(nic, CONNECTOR_10AUI);
    //TODO: try test packet
  }

  // Transceiver available is 10Base-2
  if ((options & MEDIA_OPTIONS_10BASE2_AVAILABLE) && nic->connector == CONNECTOR_UNKNOWN) {
    nic_setup_connector(nic, CONNECTOR_10BASE2);
    //TODO: try loopback packet
    execute_command(nic, CMD_DISABLE_DC_CONVERTER, 0);
    nic_check_dc_converter(nic, 0);
  }

  // Nothing left to try!
  if (nic->connector == CONNECTOR_UNKNOWN) {
    kprintf(KERN_WARNING, "nic: no connector found\n");
    nic->connector = CONNECTOR_10BASET;
    nic->linkspeed = 10;
    return -ENODEV;
  }

  nic_setup_connector(nic, nic->connector);
  return 0;
}

int nic_program_mii(struct nic *nic, int connector) {
  int phy_control;
  int phy_status;
  unsigned short mii_type;

  nictrace("enter nic_program_mii\n");

  // First see if there's anything connected to the MII
  if (!nic_find_mii_phy(nic) < 0) return -ENODEV;

  phy_control = nic_read_mii_phy(nic, MII_PHY_CONTROL);
  if (phy_control < 0) return phy_control;

  phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
  if (phy_status < 0) return phy_status;

  // Reads the miiSelect field in EEPROM. Program MII as the default.
  mii_type = nic->eeprom[EEPROM_SOFTWARE_INFO3];

  // If an override is present AND the transceiver type is available
  // on the card, that type will be used.
  //rc = nic_mii_media_override(nic, phy_status, &mii_type);
  //if (rc < 0) return rc;

  // If full duplex selected, set it in PhyControl.
  if (nic->fullduplex) {
    phy_control |= MII_CONTROL_FULL_DUPLEX;
  } else {
    phy_control &= ~MII_CONTROL_FULL_DUPLEX;
  }
  phy_control &= ~MII_CONTROL_ENABLE_AUTO;

  if (((mii_type & MIITXTYPE_MASK) == MIISELECT_100BTX) ||
      ((mii_type & MIITXTYPE_MASK) == MIISELECT_100BTX_ANE)) {
    phy_control |= MII_CONTROL_100MB;
    nic_write_mii_phy(nic, MII_PHY_CONTROL, (unsigned short) phy_control);
    msleep(600);
    nic->linkspeed = 100;
    return 0;
  } else if (((mii_type & MIITXTYPE_MASK ) == MIISELECT_10BT) ||
             ((mii_type & MIITXTYPE_MASK ) == MIISELECT_10BT_ANE)) {
    phy_control &= ~MII_CONTROL_100MB;
    nic_write_mii_phy(nic, MII_PHY_CONTROL, (unsigned short) phy_control);
    msleep(600);
    nic->linkspeed = 10;
    return 0;
  }

  phy_control &= ~MII_CONTROL_100MB;
  nic_write_mii_phy(nic, MII_PHY_CONTROL, (unsigned short) phy_control);
  msleep(600);
  nic->linkspeed = 10;

  return 0;
}

int nic_initialize_adapter(struct nic *nic) {
  unsigned short mac_control;
  int i;

  nictrace("enter nic_initialize_adapter\n");

  // TX engine handling
  execute_command_wait(nic, CMD_TX_DISABLE, 0);
  execute_command_wait(nic, CMD_TX_RESET, TX_RESET_MASK_NETWORK_RESET);

  // RX engine handling
  execute_command_wait(nic, CMD_RX_DISABLE, 0);
  execute_command_wait(nic, CMD_RX_RESET, RX_RESET_MASK_NETWORK_RESET);

  // Acknowledge any pending interrupts.
  execute_command(nic, CMD_ACKNOWLEDGE_INTERRUPT, ALL_ACK);
  
  // Clear the statistics from the hardware.
  execute_command(nic, CMD_STATISTICS_DISABLE, 0);
  clear_statistics(nic);

  // Get the MAC address from the EEPROM
  for (i = 0; i < ETHER_ADDR_LEN / 2; i++) {
    ((unsigned short *) nic->hwaddr.addr)[i] = htons(nic->eeprom[EEPROM_OEM_NODE_ADDRESS1 + i]);
  }

  // Set the card MAC address
  select_window(nic, 2);
  for (i = 0; i < ETHER_ADDR_LEN; i++) outp(nic->iobase + i, nic->hwaddr.addr[i]);

  outpw(nic->iobase + 0x6, 0);
  outpw(nic->iobase + 0x8, 0);
  outpw(nic->iobase + 0xA, 0);

  // Enable statistics
  execute_command(nic, CMD_STATISTICS_ENABLE, 0);

  // Clear the mac control register.
  select_window(nic, 3);
  mac_control = inpw(nic->iobase + MAC_CONTROL);
  mac_control &= 0x1;
  outpw(nic->iobase + MAC_CONTROL, mac_control);

  return 0;
}

int nic_get_link_speed(struct nic *nic) {
  int phy_anlpar;
  int phy_aner;
  int phy_anar;
  int phy_status;

  nictrace("enter nic_get_link_speed\n");

  phy_aner = nic_read_mii_phy(nic, MII_PHY_ANER);
  if (phy_aner < 0) return phy_aner;
  
  phy_anlpar = nic_read_mii_phy(nic, MII_PHY_ANLPAR);
  if (phy_anlpar < 0) return phy_anlpar;

  phy_anar = nic_read_mii_phy(nic, MII_PHY_ANAR);
  if (phy_anar < 0) return phy_anar;

  phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
  if (phy_status < 0) return phy_status;

  // Check to see if we've completed auto-negotiation.
  if (!(phy_status & MII_STATUS_AUTO_DONE)) return -EBUSY;

  if ((phy_anar & MII_ANAR_100TXFD) && (phy_anlpar & MII_ANLPAR_100TXFD)) {
    //nic->connector = CONNECTOR_100BASETX;
    nic->linkspeed = 100;
    nic->fullduplex = 1;
  } else if ((phy_anar & MII_ANAR_100TX) && (phy_anlpar & MII_ANLPAR_100TX)) {
    //nic->connector = CONNECTOR_100BASETX;
    nic->linkspeed = 100;
    nic->fullduplex = 0;
  } else if ((phy_anar & MII_ANAR_10TFD) && (phy_anlpar & MII_ANLPAR_10TFD)) {
    //nic->connector = CONNECTOR_10BASET;
    nic->linkspeed = 10;
    nic->fullduplex = 1;
  } else if ((phy_anar & MII_ANAR_10T) && (phy_anlpar & MII_ANLPAR_10T)) {
    //nic->connector = CONNECTOR_10BASET;
    nic->linkspeed = 10;
    nic->fullduplex = 0;
  } else if (!(phy_aner & MII_ANER_LPANABLE)) {
    // Link partner is not capable of auto-negotiation. Fall back to 10HD.
    //nic->connector = CONNECTOR_10BASET;
    nic->linkspeed = 10;
    nic->fullduplex = 0;
  } else {
    return -EINVAL;
  }

  return nic->linkspeed;
}

int nic_restart_receiver(struct nic *nic) {
  nictrace("enter nic_restart_receiver\n");

  execute_command_wait(nic, CMD_RX_DISABLE, 0);
  execute_command_wait(nic, CMD_RX_RESET, RX_RESET_MASK_NETWORK_RESET);
  execute_command_wait(nic, CMD_RX_ENABLE, 0);

  return 0;
}

int nic_restart_transmitter(struct nic *nic) {
  unsigned short media_status;
  unsigned long dma_control;
  unsigned long timeout;

  nictrace("enter nic_restart_transmitter\n");

  execute_command(nic, CMD_TX_DISABLE, 0);

  // Wait for the transmit to go quiet.
  select_window(nic, 4);

  media_status = inpw(nic->iobase + MEDIA_STATUS);
  udelay(10);

  if (media_status & MEDIA_STATUS_TX_IN_PROGRESS) {
    timeout = get_ticks() + 1 * TICKS_PER_SEC;
    while (1) {
      media_status = inpw(nic->iobase + MEDIA_STATUS);
      if (!(media_status & MEDIA_STATUS_TX_IN_PROGRESS)) break;
      if (time_after(get_ticks(), timeout)) {
        kprintf(KERN_WARNING "nic: timeout waiting for transmitter to go quiet\n");
        return -ETIMEOUT;
      }
      msleep(10);
    }
  }

  // Wait for download engine to stop
  dma_control = inpd(nic->iobase + DMA_CONTROL);
  udelay(10);

  if (dma_control & DMA_CONTROL_DOWN_IN_PROGRESS) {
    timeout = get_ticks() + 1 * TICKS_PER_SEC;
    while (1) {
      dma_control = inpd(nic->iobase + DMA_CONTROL);
      if (!(dma_control & DMA_CONTROL_DOWN_IN_PROGRESS)) break;
      if (time_after(get_ticks(), timeout)) {
        kprintf(KERN_WARNING "nic: timeout waiting for download engine to stop\n");
        return -ETIMEOUT;
      }
      msleep(10);
    }
  }

  if (execute_command_wait(nic, CMD_TX_RESET, TX_RESET_MASK_DOWN_RESET) < 0) return -ETIMEOUT;
  execute_command(nic, CMD_TX_ENABLE, 0);

  return 0;
}

int nic_flow_control(struct nic *nic) {
  unsigned short phy_anar;
  unsigned short phy_control;

  nictrace("enter nic_flow_control\n");

  phy_anar = nic_read_mii_phy(nic, MII_PHY_ANAR);
  if (phy_anar < 0) return phy_anar;
  phy_anar |= MII_ANAR_FLOWCONTROL;
  nic_write_mii_phy(nic, MII_PHY_ANAR, phy_anar);

  phy_control = nic_read_mii_phy(nic, MII_PHY_CONTROL);
  if (phy_control < 0) return phy_control;
  phy_control |= MII_CONTROL_START_AUTO;
  nic_write_mii_phy(nic, MII_PHY_CONTROL, phy_control);

  return 0;
}

int nic_configure_mii(struct nic *nic, unsigned short media_options) {
  int phy_control;
  int phy_anar;
  int phy_status;
  unsigned long timeout;

  nictrace("enter nic_configure_mii\n");

  phy_control = nic_read_mii_phy(nic, MII_PHY_CONTROL);
  if (phy_control < 0) return phy_control;

  phy_anar = nic_read_mii_phy(nic, MII_PHY_ANAR);
  if (phy_anar < 0) return phy_anar;

  // Set up speed and duplex settings in MII Control and ANAR register.
  phy_anar &= ~(MII_ANAR_100TXFD | MII_ANAR_100TX  | MII_ANAR_10TFD  | MII_ANAR_10T);

  // Set up duplex.
  if (nic->fullduplex) {
    phy_control |= MII_CONTROL_FULL_DUPLEX;
  } else {
    phy_control &= ~(MII_CONTROL_FULL_DUPLEX);
  }

  // Set up flow control.
  if (nic->flowcontrol) {
    phy_anar |= MII_ANAR_FLOWCONTROL;
  } else {
    phy_anar &= ~(MII_ANAR_FLOWCONTROL);
  }

  //
  // Set up the media options. For duplex settings, if we're set to auto-select
  // then enable both half and full-duplex settings. Otherwise, go by what's
  // been enabled for duplex mode.
  //
  
  if (media_options & MEDIA_OPTIONS_100BASETX_AVAILABLE) {
    if (nic->autoselect) {
      phy_anar |= (MII_ANAR_100TXFD | MII_ANAR_100TX);
    } else if (nic->fullduplex) {
      phy_anar |= MII_ANAR_100TXFD;
    } else {
      phy_anar |= MII_ANAR_100TX;
    }
  }

  if (media_options & MEDIA_OPTIONS_10BASET_AVAILABLE) {
    if (nic->autoselect) {
      phy_anar |= (MII_ANAR_10TFD | MII_ANAR_10T);
    } else if (nic->fullduplex) {
      phy_anar |= MII_ANAR_10TFD;
    } else {
      phy_anar |= MII_ANAR_10T;
    }
  }

  // Enable and start auto-negotiation
  phy_control |= (MII_CONTROL_ENABLE_AUTO | MII_CONTROL_START_AUTO);

  // Write the MII registers back.
  nic_write_mii_phy(nic, MII_PHY_ANAR, (unsigned short) phy_anar); 
  nic_write_mii_phy(nic, MII_PHY_CONTROL, (unsigned short) phy_control); 

  // Wait for auto-negotiation to finish.
  phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
  if (phy_status < 0) return phy_status;
  udelay(1000);

  if (!(phy_status & MII_STATUS_AUTO_DONE)) {
    timeout = get_ticks() + 3 * TICKS_PER_SEC;
    while (1) {
      phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
      if (phy_status & MII_STATUS_AUTO_DONE) break;
      if (time_after(get_ticks(), timeout)) {
        kprintf(KERN_WARNING "nic: timeout waiting for auto-negotiation to finish\n");
        return -ETIMEOUT;
      }
      msleep(10);
    }
  }

  return 0;
}

int nic_check_mii_configuration(struct nic *nic, unsigned short media_options) {
  int phy_control;
  int phy_status;
  int phy_anar;
  int new_anar;
  int rc;

  nictrace("enter nic_check_mii_configuration\n");

  // Check to see if auto-negotiation has completed.
  phy_control = nic_read_mii_phy(nic, MII_PHY_CONTROL);
  if (phy_control < 0) return phy_control;

  phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
  if (phy_status < 0) return phy_status;

  if (!((phy_control & MII_CONTROL_ENABLE_AUTO) && (phy_status & MII_STATUS_AUTO_DONE))) {
    // Auto-negotiation did not complete, so start it over using the new settings.
    rc = nic_configure_mii(nic, media_options);
    if (rc < 0) return rc;
  }
  
  //
  // Auto-negotiation has completed. Check the results against the ANAR and ANLPAR
  // registers to see if we need to restart auto-neg.
  //
  
  phy_anar = nic_read_mii_phy(nic, MII_PHY_ANAR);
  if (phy_anar < 0) return phy_anar;

  //
  // Check to see what we negotiated with the link partner. First, let's make
  // sure that the ANAR is set properly based on the media options defined.
  //
  
  new_anar = 0;
  if (media_options & MEDIA_OPTIONS_100BASETX_AVAILABLE) {
    if (nic->autoselect) {
      new_anar |= MII_ANAR_10TFD | MII_ANAR_10T;
    } else if (nic->fullduplex) {
      new_anar |= MII_ANAR_100TXFD;
    } else {
      new_anar |= MII_ANAR_100TX;
    }
  }

  if (media_options & MEDIA_OPTIONS_10BASET_AVAILABLE) {
    if (nic->autoselect) {
      new_anar |= MII_ANAR_100TXFD | MII_ANAR_100TX;
    } else if (nic->fullduplex) {
      new_anar |= MII_ANAR_10TFD;
    } else {
      new_anar |= MII_ANAR_10T;
    }
  }

  if (nic->fullduplex && nic->flowcontrol) new_anar |= MII_ANAR_FLOWCONTROL;

  // If the negotiated configuration hasn't changed return and don't restart auto-negotiation.
  if ((phy_anar & MII_ANAR_MEDIA_MASK) == new_anar) return 0;

  // Check the media settings.
  if (media_options & MEDIA_OPTIONS_100BASETX_AVAILABLE) {
    // Check 100BaseTX settings.
    if ((phy_anar & MII_ANAR_MEDIA_100_MASK) != (new_anar & MII_ANAR_MEDIA_100_MASK)) {
      return nic_configure_mii(nic, media_options);
    }
  }
  
  if (media_options & MEDIA_OPTIONS_10BASET_AVAILABLE) {
    // Check 10BaseT settings.
    if ((phy_anar & MII_ANAR_MEDIA_10_MASK) != (new_anar & MII_ANAR_MEDIA_10_MASK)) {
      return nic_configure_mii(nic, media_options);
    }
  }

  return 0;
}

int nic_check_mii_auto_negotiation_status(struct nic *nic) {
  int phy_status;
  int phy_anar;
  int phy_anlpar;

  nictrace("enter nic_check_mii_auto_negotiation_status\n");

  // Check to see if auto-negotiation has completed.
  phy_status = nic_read_mii_phy(nic, MII_PHY_STATUS);
  if (phy_status < 0) return phy_status;

  // If we have a valid link, so get out!
  if (phy_status & MII_STATUS_LINK_UP) return 0;

  //
  // Check to see why auto-negotiation or parallel detection has failed. We'll do this
  // by comparing the advertisement registers between the NIC and the link partner.
  //

  phy_anar = nic_read_mii_phy(nic, MII_PHY_ANAR);
  if (phy_anar < 0) return phy_anar;

  phy_anlpar = nic_read_mii_phy(nic, MII_PHY_ANLPAR);
  if (phy_anlpar < 0) return phy_anlpar;

  //
  // Now, compare what was advertised between the NIC and it's link partner.
  // If the media bits don't match, then write an error log entry.
  //
  if ((phy_anar & MII_ANAR_MEDIA_MASK) != (phy_anlpar & MII_ANAR_MEDIA_MASK)) {
    kprintf(KERN_ERR "nic: incompatible configuration\n");
    return -EINVAL;
  }

  return 0;
}

int nic_check_dc_converter(struct nic *nic, int enabled) {
  unsigned long timeout;
  unsigned short media_status;

  nictrace("enter nic_check_dc_converter\n");

  media_status = inpw(nic->iobase + MEDIA_STATUS);
  udelay(1000);

  if (enabled && !(media_status & MEDIA_STATUS_DC_CONVERTER_ENABLED) ||
      !enabled && (media_status & MEDIA_STATUS_DC_CONVERTER_ENABLED)) {
    timeout = get_ticks() + 3; // 30 ms
    while (1) {
      media_status = inpw(nic->iobase + MEDIA_STATUS);
      if (enabled && (media_status & MEDIA_STATUS_DC_CONVERTER_ENABLED)) break;
      if (!enabled && !(media_status & MEDIA_STATUS_DC_CONVERTER_ENABLED)) break;
      if (time_after(get_ticks(), timeout)) {
        kprintf(KERN_ERR "nic: timeout waiting for dc converter to go %s\n", enabled ? "on" : "off");
        return -ETIMEOUT;
      }
      msleep(10);
    }
  }

  return 0;
}

int nic_setup_connector(struct nic *nic, int connector) {
  unsigned long internal_config;
  unsigned long old_internal_config;
  unsigned short media_status;

  nictrace("enter nic_setup_connector\n");

  select_window(nic, 3);

  internal_config = inpd(nic->iobase + INTERNAL_CONFIG);
  old_internal_config = internal_config;

  // Program the MII registers if forcing the configuration to 10/100BaseT.
  if (connector == CONNECTOR_10BASET || connector == CONNECTOR_100BASETX) {
    // Clear transceiver type and change to new transceiver type.
    internal_config &= ~(INTERNAL_CONFIG_TRANSCEIVER_MASK);
    internal_config |= (CONNECTOR_AUTONEGOTIATION << 20);

    // Update the internal config register. Only do this if the value has
    // changed to avoid dropping link.
    if (old_internal_config != internal_config) {
      outpd(nic->iobase + INTERNAL_CONFIG, internal_config);
    }

    // Force the MII registers to the correct settings.
    if (nic_check_mii_configuration(nic, (unsigned short) (connector == CONNECTOR_100BASETX ? MEDIA_OPTIONS_100BASETX_AVAILABLE : MEDIA_OPTIONS_10BASET_AVAILABLE)) < 0) {
      // If the forced configuration didn't work, check the results and see why.
      return nic_check_mii_auto_negotiation_status(nic);
    }
  } else {
    // Clear transceiver type and change to new transceiver type
    internal_config = internal_config & (~INTERNAL_CONFIG_TRANSCEIVER_MASK);
    internal_config |= (connector << 20);

    // Update the internal config register. Only do this if the value has
    // changed to avoid dropping link.
    if (old_internal_config != internal_config) {
      outpd(nic->iobase + INTERNAL_CONFIG, internal_config);
    }
  }

  //
  // Determine whether to set enableSQEStats and linkBeatEnable
  // Automatically set JabberGuardEnable in MediaStatus register.
  //
  select_window(nic, 4);

  media_status = inpw(nic->iobase + MEDIA_STATUS);
  media_status &= ~(MEDIA_STATUS_SQE_STATISTICS_ENABLE | MEDIA_STATUS_LINK_BEAT_ENABLE | MEDIA_STATUS_JABBER_GUARD_ENABLE);
  media_status |= MEDIA_STATUS_JABBER_GUARD_ENABLE;

  if (connector == CONNECTOR_10AUI) media_status |= MEDIA_STATUS_SQE_STATISTICS_ENABLE;

  if (connector == CONNECTOR_AUTONEGOTIATION) {
    media_status |= MEDIA_STATUS_LINK_BEAT_ENABLE;
  } else {
    if (connector == CONNECTOR_10BASET || connector == CONNECTOR_100BASETX || connector == CONNECTOR_100BASEFX) {
      if (!(nic->eeprom[EEPROM_SOFTWARE_INFO1] & LINK_BEAT_DISABLE)) {
        media_status |= MEDIA_STATUS_LINK_BEAT_ENABLE;
      }
    }
  }

  outpw(nic->iobase + MEDIA_STATUS, media_status);

  //
  // If configured for coax we must start the internal transceiver.
  // If not, we stop it (in case the configuration changed across a
  // warm boot).
  //
  if (connector == CONNECTOR_10BASE2) {
    execute_command(nic, CMD_ENABLE_DC_CONVERTER, 0);
    nic_check_dc_converter(nic, 1);
  } else {
    execute_command(nic, CMD_DISABLE_DC_CONVERTER, 0);
    nic_check_dc_converter(nic, 0);
  }

  return 0;
}

int nic_setup_media(struct nic *nic) {
  unsigned short options_available;
  unsigned long internal_config;
  unsigned short mac_control;
  int rc;

  nictrace("enter nic_setup_media\n");

  // If this is a 10mb Lightning card, assume that the 10FL bit is
  // set in the media options register
  if (nic->deviceid == 0x900A) {
    options_available = MEDIA_OPTIONS_10BASEFL_AVAILABLE;
  } else {
    // Read the MEDIA OPTIONS to see what connectors are available
    select_window(nic, 3);
    options_available = inpw(nic->iobase + MEDIA_OPTIONS);
  }

  // Get internal config from EEPROM since reset invalidates the normal register value.
  internal_config = nic->eeprom[EEPROM_INTERNAL_CONFIG0] | (nic->eeprom[EEPROM_INTERNAL_CONFIG1] << 16);

  //
  // Read the current value of the InternalConfig register. If it's different
  // from the EEPROM values, than write it out using the EEPROM values.
  // This is done since a global reset may invalidate the register value on
  // some ASICs. Also, writing to InternalConfig may reset the PHY on some ASICs.
  //

  select_window(nic, 3);

  if (internal_config != inpd(nic->iobase + INTERNAL_CONFIG)) {
    outpd(nic->iobase + INTERNAL_CONFIG, internal_config);
  }

  // Get the connector to use.
  if (nic->connector == CONNECTOR_UNKNOWN) {
    nic->connector = (internal_config & INTERNAL_CONFIG_TRANSCEIVER_MASK) >> INTERNAL_CONFIG_TRANSCEIVER_SHIFT;
    if (internal_config & INTERNAL_CONFIG_AUTO_SELECT) nic->autoselect = 1;
  }

  // If auto selection of connector was specified, do it now...
  if (nic->connector == CONNECTOR_AUTONEGOTIATION) {
    execute_command(nic, CMD_STATISTICS_DISABLE, 0);
    nic_negotiate_link(nic, options_available);
  } else {
    // MII connector needs to be initialized and the data rates
    // set up even in the non-autoselect case
    if (nic->connector == CONNECTOR_MII) {
      nic_program_mii(nic, CONNECTOR_MII);
    } else {
      if (nic->connector == CONNECTOR_100BASEFX || nic->connector == CONNECTOR_100BASETX) {
        nic->linkspeed = 100;
      } else {
        nic->linkspeed = 10;
      }
    }

    nic_setup_connector(nic, nic->connector);
  }

  //
  // Check link speed and duplex settings before doing anything else.
  // If the call succeeds, we know the link is up, so we'll update the
  // link state.
  //
  if (nic_get_link_speed(nic) < 0) return -EIO;
  
  // Set up duplex mode
  select_window(nic, 3);
  mac_control = inpw(nic->iobase +  MAC_CONTROL);
  if (nic->fullduplex) {
    // Set Full duplex in MacControl register
    mac_control |= MAC_CONTROL_FULL_DUPLEX_ENABLE;

    // Since we're switching to full duplex, enable flow control.
    mac_control |=  MAC_CONTROL_FLOW_CONTROL_ENABLE;
    nic->flowcontrol = 1;
  } else {
    // Set Half duplex in MacControl register
    mac_control &= ~MAC_CONTROL_FULL_DUPLEX_ENABLE;

    // Since we're switching to half duplex, disable flow control
    mac_control &= ~ MAC_CONTROL_FLOW_CONTROL_ENABLE;
  }
  outpw(nic->iobase + MAC_CONTROL, mac_control);

  // Reset and enable transmitter
  rc = nic_restart_transmitter(nic);
  if (rc < 0) return rc;

  // Reset and enable receiver
  rc = nic_restart_receiver(nic);
  if (rc < 0) return rc;

  //
  // This is for advertisement of flow control.  We only need to
  // call this if the adapter is using flow control, in Autoselect
  // mode and not a Tornado board.
  //
  if (nic->autoselect && nic->flowcontrol && nic->deviceid != 0x9200 && nic->deviceid != 0x9201 && nic->deviceid != 0x9805) {
    nic_flow_control(nic);
  }

  return 0;
}

int nic_software_work(struct nic *nic) {
  unsigned long dma_control;
  unsigned short net_diag;
  int phy_reg;

  nictrace("enter nic_software_work\n");

  if (!(nic->eeprom[EEPROM_SOFTWARE_INFO2] & ENABLE_MWI_WORK)) {
    dma_control = inpd(nic->iobase + DMA_CONTROL);
    outpd(nic->iobase + DMA_CONTROL, dma_control | DMA_CONTROL_DEFEAT_MWI);
  }

  select_window(nic, 4);
  net_diag = inpw(nic->iobase + NETWORK_DIAGNOSTICS);

  if ((((net_diag & NETWORK_DIAGNOSTICS_ASIC_REVISION) >> 4) == 1) &&
      (((net_diag & NETWORK_DIAGNOSTICS_ASIC_REVISION_LOW) >> 1) < 4)) {
    phy_reg = nic_read_mii_phy(nic, 24);
    phy_reg |= 1;
    nic_write_mii_phy(nic, 24, (unsigned short) phy_reg);
  }

  return 0;
}

int nic_setup_buffers(struct nic *nic) {
  int i;

  nictrace("enter nic_setup_buffers\n");

  // Setup the receive ring
  for (i = 0; i < RX_RING_SIZE; i++) {
    if (i == RX_RING_SIZE - 1) {
      nic->rx_ring[i].next = &nic->rx_ring[0];
    } else {
      nic->rx_ring[i].next = &nic->rx_ring[i + 1];
    }

    nic->rx_ring[i].pkt = pbuf_alloc(PBUF_RAW, ETHER_FRAME_LEN, PBUF_RW);
    if (!nic->rx_ring[i].pkt) return -ENOMEM;

    nic->rx_ring[i].phys_next = virt2phys(nic->rx_ring[i].next);
    nic->rx_ring[i].sglist[0].addr = virt2phys(nic->rx_ring[i].pkt->payload);
    nic->rx_ring[i].sglist[0].length = ETHER_FRAME_LEN | LAST_FRAG;
  }
  nic->curr_rx = &nic->rx_ring[0];

  // Setup the transmit ring
  for (i = 0; i < TX_RING_SIZE; i++) {
    if (i == TX_RING_SIZE - 1) {
      nic->tx_ring[i].next = &nic->tx_ring[0];
    } else {
      nic->tx_ring[i].next = &nic->tx_ring[i + 1];
    }

    if (i == 0) {
      nic->tx_ring[i].prev = &nic->tx_ring[TX_RING_SIZE - 1];
    } else {
      nic->tx_ring[i].prev = &nic->tx_ring[i - 1];
    }

    nic->tx_ring[i].phys_addr = virt2phys(&nic->tx_ring[i]);
  }

  nic->next_tx = &nic->tx_ring[0];
  nic->next_tx->header |= FSH_DPD_EMPTY;

  nic->tx_size = 0;
  nic->curr_tx = &nic->tx_ring[0];

  return 0;
}

int nic_start_adapter(struct nic *nic) {
  unsigned short diagnostics;
  unsigned long dma_control;

  nictrace("enter nic_start_adapter\n");

  // Enable upper bytes counting in diagnostics register.
  select_window(nic, 4);

  diagnostics = inpw(nic->iobase + NETWORK_DIAGNOSTICS);
  diagnostics |= NETWORK_DIAGNOSTICS_UPPER_BYTES_ENABLE;
  outpw(nic->iobase + NETWORK_DIAGNOSTICS, diagnostics);

  // Enable counter speed in DMA control.
  dma_control = inpd(nic->iobase + DMA_CONTROL);
  if (nic->linkspeed == 100) dma_control |= DMA_CONTROL_COUNTER_SPEED;
  outpd(nic->iobase + DMA_CONTROL, dma_control);

  // Give receive ring to upload engine
  execute_command_wait(nic, CMD_UP_STALL, 0);
  outpd(nic->iobase + UP_LIST_POINTER, virt2phys(nic->curr_rx));
  execute_command(nic, CMD_UP_UNSTALL, 0);

  // Give transmit ring to download engine
  init_sem(&nic->tx_sem, TX_RING_SIZE);
  execute_command_wait(nic, CMD_DOWN_STALL, 0);
  outpd(nic->iobase + DOWN_LIST_POINTER, 0);
  execute_command(nic, CMD_DOWN_UNSTALL, 0);

  // Set receive filter
  execute_command(nic, CMD_SET_RX_FILTER, RECEIVE_INDIVIDUAL /*| RECEIVE_MULTICAST*/ | RECEIVE_BROADCAST);

  // Enable the statistics back.
  execute_command(nic, CMD_STATISTICS_ENABLE, 0);

  // Acknowledge any pending interrupts.
  execute_command(nic, CMD_ACKNOWLEDGE_INTERRUPT, ALL_ACK);

  // Enable indication for all interrupts.
  execute_command(nic, CMD_SET_INDICATION_ENABLE, ALL_INTERRUPTS);

  // Enable all interrupts to the host.
  execute_command(nic, CMD_SET_INTERRUPT_ENABLE, ALL_INTERRUPTS);

  // Enable the transmit and receive engines.
  execute_command(nic, CMD_RX_ENABLE, 0);
  execute_command(nic, CMD_TX_ENABLE, 0);

  // Delay three seconds, only some switches need this
  //msleep(3000);

  nictrace("exit nic_start_adapter\n");

  return 0;
}

int __declspec(dllexport) install(struct unit *unit) {
  struct nic *nic;
  int i;
  int value;
  int rc;

  // Check for PCI device
  if (unit->bus->bustype != BUSTYPE_PCI) return -EINVAL;

  // Determine chipset
  switch (unit->unitcode) {
    case UNITCODE_3C905B1: 
      unit->vendorname = "3Com";
      unit->productname = "3Com EtherLink 3C905B-1";
      break;

    case UNITCODE_3C905C:
      unit->vendorname = "3Com";
      unit->productname = "3Com EtherLink 3C905C";
      break;

    case UNITCODE_3C9051:
      unit->vendorname = "3Com";
      unit->productname = "3Com EtherLink 3C905-1";
      break;

    default:
      unit->vendorname = "3Com";
      unit->productname = "3Com EtherLink 3C90xC";
  }

  // Allocate device structure
  nic = (struct nic *) kmalloc(sizeof(struct nic));
  if (!nic) return -ENOMEM;
  memset(nic, 0, sizeof(struct nic));

  // Setup NIC configuration
  nic->iobase = (unsigned short) get_unit_iobase(unit);
  nic->irq = (unsigned short) get_unit_irq(unit);
  nic->deviceid = PCI_DEVICE_ID(unit->unitcode);
  nic->connector = CONNECTOR_UNKNOWN;
  nic->devno = dev_make("eth#", &nic_driver, unit, nic);

  // Enable bus mastering
  pci_enable_busmastering(unit);

  // Install interrupt handler
  init_dpc(&nic->dpc);
  register_interrupt(&nic->intr, IRQ2INTR(nic->irq), nic_handler, nic);
  enable_irq(nic->irq);

  // Setup buffers
  rc = nic_setup_buffers(nic);
  if (rc < 0) return rc;

  // Global reset
  rc = execute_command_wait(nic, CMD_RESET, 
         GLOBAL_RESET_MASK_TP_AUI_RESET | 
         GLOBAL_RESET_MASK_ENDEC_RESET | 
         GLOBAL_RESET_MASK_AISM_RESET | 
         GLOBAL_RESET_MASK_SMB_RESET | 
         GLOBAL_RESET_MASK_VCO_RESET);

  if (rc < 0) {
    kprintf(KERN_ERR, "nic: wait asic ready timeout\n");
    return -EIO;
  }

  // Read the EEPROM
  for (i = 0; i < EEPROM_SIZE; i++) {
    value = nic_read_eeprom(nic, (unsigned short) i);
    if (value < 0) return value;
    nic->eeprom[i] = value;
  }

  // Initialize adapter
  rc = nic_initialize_adapter(nic);
  if (rc < 0) return rc;

  // Setup media
  rc = nic_setup_media(nic);
  if (rc < 0) return rc;

  // Software work
  rc = nic_software_work(nic);
  if (rc < 0) return rc;

  // Start adapter
  rc = nic_start_adapter(nic);
  if (rc < 0) return rc;

  register_proc_inode(device(nic->devno)->name, nicstat_proc, nic);

  kprintf(KERN_INFO "%s: %s, iobase 0x%x irq %d mac %la\n", device(nic->devno)->name, unit->productname, nic->iobase, nic->irq, &nic->hwaddr);
  kprintf(KERN_INFO "%s: %d MBits/s, %s-duplex, %s\n", device(nic->devno)->name, nic->linkspeed, nic->fullduplex ? "full" : "half", connectorname[nic->connector]);

  return 0;
}

int __stdcall start(hmodule_t hmod, int reason, void *reserved2) {
  netstats = get_netstats();
  return 1;
}