Skip to content
Snippets Groups Projects
b44.c 57.9 KiB
Newer Older
/* b44.c: Broadcom 44xx/47xx Fast Ethernet device driver.
Linus Torvalds's avatar
Linus Torvalds committed
 *
 * Copyright (C) 2002 David S. Miller (davem@redhat.com)
 * Copyright (C) 2004 Pekka Pietikainen (pp@ee.oulu.fi)
 * Copyright (C) 2004 Florian Schirmer (jolt@tuxbox.org)
 * Copyright (C) 2006 Felix Fietkau (nbd@openwrt.org)
 * Copyright (C) 2006 Broadcom Corporation.
Michael Buesch's avatar
Michael Buesch committed
 * Copyright (C) 2007 Michael Buesch <m@bues.ch>
Linus Torvalds's avatar
Linus Torvalds committed
 *
 * Distribute under GPL.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

Linus Torvalds's avatar
Linus Torvalds committed
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/types.h>
#include <linux/netdevice.h>
#include <linux/ethtool.h>
#include <linux/mii.h>
#include <linux/if_ether.h>
#include <linux/if_vlan.h>
Linus Torvalds's avatar
Linus Torvalds committed
#include <linux/etherdevice.h>
#include <linux/pci.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/dma-mapping.h>
#include <linux/ssb/ssb.h>
Linus Torvalds's avatar
Linus Torvalds committed

#include <asm/uaccess.h>
#include <asm/io.h>
#include <asm/irq.h>

Linus Torvalds's avatar
Linus Torvalds committed
#include "b44.h"

#define DRV_MODULE_NAME		"b44"
#define DRV_MODULE_VERSION	"2.0"
#define DRV_DESCRIPTION		"Broadcom 44xx/47xx 10/100 PCI ethernet driver"
Linus Torvalds's avatar
Linus Torvalds committed

#define B44_DEF_MSG_ENABLE	  \
	(NETIF_MSG_DRV		| \
	 NETIF_MSG_PROBE	| \
	 NETIF_MSG_LINK		| \
	 NETIF_MSG_TIMER	| \
	 NETIF_MSG_IFDOWN	| \
	 NETIF_MSG_IFUP		| \
	 NETIF_MSG_RX_ERR	| \
	 NETIF_MSG_TX_ERR)

/* length of time before we decide the hardware is borked,
 * and dev->tx_timeout() should be called to fix the problem
 */
#define B44_TX_TIMEOUT			(5 * HZ)

/* hardware minimum and maximum for a single frame's data payload */
#define B44_MIN_MTU			60
#define B44_MAX_MTU			1500

#define B44_RX_RING_SIZE		512
#define B44_DEF_RX_RING_PENDING		200
#define B44_RX_RING_BYTES	(sizeof(struct dma_desc) * \
				 B44_RX_RING_SIZE)
#define B44_TX_RING_SIZE		512
#define B44_DEF_TX_RING_PENDING		(B44_TX_RING_SIZE - 1)
#define B44_TX_RING_BYTES	(sizeof(struct dma_desc) * \
				 B44_TX_RING_SIZE)

#define TX_RING_GAP(BP)	\
	(B44_TX_RING_SIZE - (BP)->tx_pending)
#define TX_BUFFS_AVAIL(BP)						\
	(((BP)->tx_cons <= (BP)->tx_prod) ?				\
	  (BP)->tx_cons + (BP)->tx_pending - (BP)->tx_prod :		\
	  (BP)->tx_cons - (BP)->tx_prod - TX_RING_GAP(BP))
#define NEXT_TX(N)		(((N) + 1) & (B44_TX_RING_SIZE - 1))

#define RX_PKT_OFFSET		(RX_HEADER_LEN + 2)
#define RX_PKT_BUF_SZ		(1536 + RX_PKT_OFFSET)
Linus Torvalds's avatar
Linus Torvalds committed

/* minimum number of free TX descriptors required to wake up TX process */
#define B44_TX_WAKEUP_THRESH		(B44_TX_RING_SIZE / 4)

/* b44 internal pattern match filter info */
#define B44_PATTERN_BASE	0x400
#define B44_PATTERN_SIZE	0x80
#define B44_PMASK_BASE		0x600
#define B44_PMASK_SIZE		0x10
#define B44_MAX_PATTERNS	16
#define B44_ETHIPV6UDP_HLEN	62
#define B44_ETHIPV4UDP_HLEN	42

MODULE_AUTHOR("Felix Fietkau, Florian Schirmer, Pekka Pietikainen, David S. Miller");
MODULE_DESCRIPTION(DRV_DESCRIPTION);
Linus Torvalds's avatar
Linus Torvalds committed
MODULE_LICENSE("GPL");
MODULE_VERSION(DRV_MODULE_VERSION);

static int b44_debug = -1;	/* -1 == use B44_DEF_MSG_ENABLE as value */
module_param(b44_debug, int, 0);
MODULE_PARM_DESC(b44_debug, "B44 bitmapped debugging message enable value");


#ifdef CONFIG_B44_PCI
static DEFINE_PCI_DEVICE_TABLE(b44_pci_tbl) = {
	{ PCI_DEVICE(PCI_VENDOR_ID_BROADCOM, PCI_DEVICE_ID_BCM4401) },
	{ PCI_DEVICE(PCI_VENDOR_ID_BROADCOM, PCI_DEVICE_ID_BCM4401B0) },
	{ PCI_DEVICE(PCI_VENDOR_ID_BROADCOM, PCI_DEVICE_ID_BCM4401B1) },
	{ 0 } /* terminate list with empty entry */
};
Linus Torvalds's avatar
Linus Torvalds committed
MODULE_DEVICE_TABLE(pci, b44_pci_tbl);

static struct pci_driver b44_pci_driver = {
	.name		= DRV_MODULE_NAME,
	.id_table	= b44_pci_tbl,
};
#endif /* CONFIG_B44_PCI */

static const struct ssb_device_id b44_ssb_tbl[] = {
	SSB_DEVICE(SSB_VENDOR_BROADCOM, SSB_DEV_ETHERNET, SSB_ANY_REV),
	SSB_DEVTABLE_END
};
MODULE_DEVICE_TABLE(ssb, b44_ssb_tbl);

Linus Torvalds's avatar
Linus Torvalds committed
static void b44_halt(struct b44 *);
static void b44_init_rings(struct b44 *);

#define B44_FULL_RESET		1
#define B44_FULL_RESET_SKIP_PHY	2
#define B44_PARTIAL_RESET	3
#define B44_CHIP_RESET_FULL	4
#define B44_CHIP_RESET_PARTIAL	5
static void b44_init_hw(struct b44 *, int);
Linus Torvalds's avatar
Linus Torvalds committed

static int dma_desc_sync_size;
static int instance;
static const char b44_gstrings[][ETH_GSTRING_LEN] = {
#define _B44(x...)	# x,
B44_STAT_REG_DECLARE
#undef _B44
};

static inline void b44_sync_dma_desc_for_device(struct ssb_device *sdev,
						dma_addr_t dma_base,
						unsigned long offset,
						enum dma_data_direction dir)
	dma_sync_single_for_device(sdev->dma_dev, dma_base + offset,
				   dma_desc_sync_size, dir);
static inline void b44_sync_dma_desc_for_cpu(struct ssb_device *sdev,
					     dma_addr_t dma_base,
					     unsigned long offset,
					     enum dma_data_direction dir)
	dma_sync_single_for_cpu(sdev->dma_dev, dma_base + offset,
				dma_desc_sync_size, dir);
Linus Torvalds's avatar
Linus Torvalds committed
static inline unsigned long br32(const struct b44 *bp, unsigned long reg)
{
	return ssb_read32(bp->sdev, reg);
static inline void bw32(const struct b44 *bp,
Linus Torvalds's avatar
Linus Torvalds committed
			unsigned long reg, unsigned long val)
{
	ssb_write32(bp->sdev, reg, val);
Linus Torvalds's avatar
Linus Torvalds committed
}

static int b44_wait_bit(struct b44 *bp, unsigned long reg,
			u32 bit, unsigned long timeout, const int clear)
{
	unsigned long i;

	for (i = 0; i < timeout; i++) {
		u32 val = br32(bp, reg);

		if (clear && !(val & bit))
			break;
		if (!clear && (val & bit))
			break;
		udelay(10);
	}
	if (i == timeout) {
		if (net_ratelimit())
			netdev_err(bp->dev, "BUG!  Timeout waiting for bit %08x of register %lx to %s\n",
				   bit, reg, clear ? "clear" : "set");

Linus Torvalds's avatar
Linus Torvalds committed
		return -ENODEV;
	}
	return 0;
}

static inline void __b44_cam_read(struct b44 *bp, unsigned char *data, int index)
Linus Torvalds's avatar
Linus Torvalds committed
{
	u32 val;

	bw32(bp, B44_CAM_CTRL, (CAM_CTRL_READ |
			    (index << CAM_CTRL_INDEX_SHIFT)));
Linus Torvalds's avatar
Linus Torvalds committed

	b44_wait_bit(bp, B44_CAM_CTRL, CAM_CTRL_BUSY, 100, 1);
Linus Torvalds's avatar
Linus Torvalds committed

	val = br32(bp, B44_CAM_DATA_LO);
Linus Torvalds's avatar
Linus Torvalds committed

	data[2] = (val >> 24) & 0xFF;
	data[3] = (val >> 16) & 0xFF;
	data[4] = (val >> 8) & 0xFF;
	data[5] = (val >> 0) & 0xFF;
Linus Torvalds's avatar
Linus Torvalds committed

	val = br32(bp, B44_CAM_DATA_HI);
Linus Torvalds's avatar
Linus Torvalds committed

	data[0] = (val >> 8) & 0xFF;
	data[1] = (val >> 0) & 0xFF;
static inline void __b44_cam_write(struct b44 *bp, unsigned char *data, int index)
Linus Torvalds's avatar
Linus Torvalds committed
{
	u32 val;

	val  = ((u32) data[2]) << 24;
	val |= ((u32) data[3]) << 16;
	val |= ((u32) data[4]) <<  8;
	val |= ((u32) data[5]) <<  0;
	bw32(bp, B44_CAM_DATA_LO, val);
	val = (CAM_DATA_HI_VALID |
Linus Torvalds's avatar
Linus Torvalds committed
	       (((u32) data[0]) << 8) |
	       (((u32) data[1]) << 0));
	bw32(bp, B44_CAM_DATA_HI, val);
	bw32(bp, B44_CAM_CTRL, (CAM_CTRL_WRITE |
			    (index << CAM_CTRL_INDEX_SHIFT)));
	b44_wait_bit(bp, B44_CAM_CTRL, CAM_CTRL_BUSY, 100, 1);
Linus Torvalds's avatar
Linus Torvalds committed
}

static inline void __b44_disable_ints(struct b44 *bp)
{
	bw32(bp, B44_IMASK, 0);
}

static void b44_disable_ints(struct b44 *bp)
{
	__b44_disable_ints(bp);

	/* Flush posted writes. */
	br32(bp, B44_IMASK);
}

static void b44_enable_ints(struct b44 *bp)
{
	bw32(bp, B44_IMASK, bp->imask);
}

static int __b44_readphy(struct b44 *bp, int phy_addr, int reg, u32 *val)
Linus Torvalds's avatar
Linus Torvalds committed
{
	int err;

	bw32(bp, B44_EMAC_ISTAT, EMAC_INT_MII);
	bw32(bp, B44_MDIO_DATA, (MDIO_DATA_SB_START |
			     (MDIO_OP_READ << MDIO_DATA_OP_SHIFT) |
			     (phy_addr << MDIO_DATA_PMD_SHIFT) |
Linus Torvalds's avatar
Linus Torvalds committed
			     (reg << MDIO_DATA_RA_SHIFT) |
			     (MDIO_TA_VALID << MDIO_DATA_TA_SHIFT)));
	err = b44_wait_bit(bp, B44_EMAC_ISTAT, EMAC_INT_MII, 100, 0);
	*val = br32(bp, B44_MDIO_DATA) & MDIO_DATA_DATA;

	return err;
}

static int __b44_writephy(struct b44 *bp, int phy_addr, int reg, u32 val)
Linus Torvalds's avatar
Linus Torvalds committed
{
	bw32(bp, B44_EMAC_ISTAT, EMAC_INT_MII);
	bw32(bp, B44_MDIO_DATA, (MDIO_DATA_SB_START |
			     (MDIO_OP_WRITE << MDIO_DATA_OP_SHIFT) |
			     (phy_addr << MDIO_DATA_PMD_SHIFT) |
Linus Torvalds's avatar
Linus Torvalds committed
			     (reg << MDIO_DATA_RA_SHIFT) |
			     (MDIO_TA_VALID << MDIO_DATA_TA_SHIFT) |
			     (val & MDIO_DATA_DATA)));
	return b44_wait_bit(bp, B44_EMAC_ISTAT, EMAC_INT_MII, 100, 0);
}

static inline int b44_readphy(struct b44 *bp, int reg, u32 *val)
{
	if (bp->phy_addr == B44_PHY_ADDR_NO_PHY)
		return 0;

	return __b44_readphy(bp, bp->phy_addr, reg, val);
}

static inline int b44_writephy(struct b44 *bp, int reg, u32 val)
{
	if (bp->phy_addr == B44_PHY_ADDR_NO_PHY)
		return 0;

	return __b44_writephy(bp, bp->phy_addr, reg, val);
}

Linus Torvalds's avatar
Linus Torvalds committed
/* miilib interface */
static int b44_mii_read(struct net_device *dev, int phy_id, int location)
{
	u32 val;
	struct b44 *bp = netdev_priv(dev);
	int rc = __b44_readphy(bp, phy_id, location, &val);
Linus Torvalds's avatar
Linus Torvalds committed
	if (rc)
		return 0xffffffff;
	return val;
}

static void b44_mii_write(struct net_device *dev, int phy_id, int location,
			 int val)
{
	struct b44 *bp = netdev_priv(dev);
	__b44_writephy(bp, phy_id, location, val);
Linus Torvalds's avatar
Linus Torvalds committed
}

static int b44_phy_reset(struct b44 *bp)
{
	u32 val;
	int err;

	if (bp->phy_addr == B44_PHY_ADDR_NO_PHY)
		return 0;
Linus Torvalds's avatar
Linus Torvalds committed
	err = b44_writephy(bp, MII_BMCR, BMCR_RESET);
	if (err)
		return err;
	udelay(100);
	err = b44_readphy(bp, MII_BMCR, &val);
	if (!err) {
		if (val & BMCR_RESET) {
			netdev_err(bp->dev, "PHY Reset would not complete\n");
Linus Torvalds's avatar
Linus Torvalds committed
			err = -ENODEV;
		}
	}

Linus Torvalds's avatar
Linus Torvalds committed
}

static void __b44_set_flow_ctrl(struct b44 *bp, u32 pause_flags)
{
	u32 val;

	bp->flags &= ~(B44_FLAG_TX_PAUSE | B44_FLAG_RX_PAUSE);
	bp->flags |= pause_flags;

	val = br32(bp, B44_RXCONFIG);
	if (pause_flags & B44_FLAG_RX_PAUSE)
		val |= RXCONFIG_FLOW;
	else
		val &= ~RXCONFIG_FLOW;
	bw32(bp, B44_RXCONFIG, val);

	val = br32(bp, B44_MAC_FLOW);
	if (pause_flags & B44_FLAG_TX_PAUSE)
		val |= (MAC_FLOW_PAUSE_ENAB |
			(0xc0 & MAC_FLOW_RX_HI_WATER));
	else
		val &= ~MAC_FLOW_PAUSE_ENAB;
	bw32(bp, B44_MAC_FLOW, val);
}

static void b44_set_flow_ctrl(struct b44 *bp, u32 local, u32 remote)
{
	u32 pause_enab = 0;

	/* The driver supports only rx pause by default because
	   the b44 mac tx pause mechanism generates excessive
	   pause frames.
	   Use ethtool to turn on b44 tx pause if necessary.
	 */
	if ((local & ADVERTISE_PAUSE_CAP) &&
	    (local & ADVERTISE_PAUSE_ASYM)){
		if ((remote & LPA_PAUSE_ASYM) &&
		    !(remote & LPA_PAUSE_CAP))
			pause_enab |= B44_FLAG_RX_PAUSE;
Linus Torvalds's avatar
Linus Torvalds committed
	}

	__b44_set_flow_ctrl(bp, pause_enab);
}

#ifdef CONFIG_BCM47XX
#include <asm/mach-bcm47xx/nvram.h>
static void b44_wap54g10_workaround(struct b44 *bp)
{
	char buf[20];
	u32 val;
	int err;

	/*
	 * workaround for bad hardware design in Linksys WAP54G v1.0
	 * see https://dev.openwrt.org/ticket/146
	 * check and reset bit "isolate"
	 */
	if (nvram_getenv("boardnum", buf, sizeof(buf)) < 0)
	if (simple_strtoul(buf, NULL, 0) == 2) {
		err = __b44_readphy(bp, 0, MII_BMCR, &val);
		if (err)
			goto error;
		if (!(val & BMCR_ISOLATE))
			return;
		val &= ~BMCR_ISOLATE;
		err = __b44_writephy(bp, 0, MII_BMCR, val);
		if (err)
			goto error;
	}
	return;
error:
	pr_warning("PHY: cannot reset MII transceiver isolate bit\n");
}
#else
static inline void b44_wap54g10_workaround(struct b44 *bp)
{
}
#endif

Linus Torvalds's avatar
Linus Torvalds committed
static int b44_setup_phy(struct b44 *bp)
{
	u32 val;
	int err;

	b44_wap54g10_workaround(bp);

	if (bp->phy_addr == B44_PHY_ADDR_NO_PHY)
		return 0;
Linus Torvalds's avatar
Linus Torvalds committed
	if ((err = b44_readphy(bp, B44_MII_ALEDCTRL, &val)) != 0)
		goto out;
	if ((err = b44_writephy(bp, B44_MII_ALEDCTRL,
				val & MII_ALEDCTRL_ALLMSK)) != 0)
		goto out;
	if ((err = b44_readphy(bp, B44_MII_TLEDCTRL, &val)) != 0)
		goto out;
	if ((err = b44_writephy(bp, B44_MII_TLEDCTRL,
				val | MII_TLEDCTRL_ENABLE)) != 0)
		goto out;

	if (!(bp->flags & B44_FLAG_FORCE_LINK)) {
		u32 adv = ADVERTISE_CSMA;

		if (bp->flags & B44_FLAG_ADV_10HALF)
			adv |= ADVERTISE_10HALF;
		if (bp->flags & B44_FLAG_ADV_10FULL)
			adv |= ADVERTISE_10FULL;
		if (bp->flags & B44_FLAG_ADV_100HALF)
			adv |= ADVERTISE_100HALF;
		if (bp->flags & B44_FLAG_ADV_100FULL)
			adv |= ADVERTISE_100FULL;

		if (bp->flags & B44_FLAG_PAUSE_AUTO)
			adv |= ADVERTISE_PAUSE_CAP | ADVERTISE_PAUSE_ASYM;

		if ((err = b44_writephy(bp, MII_ADVERTISE, adv)) != 0)
			goto out;
		if ((err = b44_writephy(bp, MII_BMCR, (BMCR_ANENABLE |
						       BMCR_ANRESTART))) != 0)
			goto out;
	} else {
		u32 bmcr;

		if ((err = b44_readphy(bp, MII_BMCR, &bmcr)) != 0)
			goto out;
		bmcr &= ~(BMCR_FULLDPLX | BMCR_ANENABLE | BMCR_SPEED100);
		if (bp->flags & B44_FLAG_100_BASE_T)
			bmcr |= BMCR_SPEED100;
		if (bp->flags & B44_FLAG_FULL_DUPLEX)
			bmcr |= BMCR_FULLDPLX;
		if ((err = b44_writephy(bp, MII_BMCR, bmcr)) != 0)
			goto out;

		/* Since we will not be negotiating there is no safe way
		 * to determine if the link partner supports flow control
		 * or not.  So just disable it completely in this case.
		 */
		b44_set_flow_ctrl(bp, 0, 0);
	}

out:
	return err;
}

static void b44_stats_update(struct b44 *bp)
{
	unsigned long reg;
	u32 *val;

	val = &bp->hw_stats.tx_good_octets;
	for (reg = B44_TX_GOOD_O; reg <= B44_TX_PAUSE; reg += 4UL) {
		*val++ += br32(bp, reg);
	}
Linus Torvalds's avatar
Linus Torvalds committed
	for (reg = B44_RX_GOOD_O; reg <= B44_RX_NPAUSE; reg += 4UL) {
		*val++ += br32(bp, reg);
	}
}

static void b44_link_report(struct b44 *bp)
{
	if (!netif_carrier_ok(bp->dev)) {
		netdev_info(bp->dev, "Link is down\n");
Linus Torvalds's avatar
Linus Torvalds committed
	} else {
		netdev_info(bp->dev, "Link is up at %d Mbps, %s duplex\n",
			    (bp->flags & B44_FLAG_100_BASE_T) ? 100 : 10,
			    (bp->flags & B44_FLAG_FULL_DUPLEX) ? "full" : "half");

		netdev_info(bp->dev, "Flow control is %s for TX and %s for RX\n",
			    (bp->flags & B44_FLAG_TX_PAUSE) ? "on" : "off",
			    (bp->flags & B44_FLAG_RX_PAUSE) ? "on" : "off");
Linus Torvalds's avatar
Linus Torvalds committed
	}
}

static void b44_check_phy(struct b44 *bp)
{
	u32 bmsr, aux;

	if (bp->phy_addr == B44_PHY_ADDR_NO_PHY) {
		bp->flags |= B44_FLAG_100_BASE_T;
		bp->flags |= B44_FLAG_FULL_DUPLEX;
		if (!netif_carrier_ok(bp->dev)) {
			u32 val = br32(bp, B44_TX_CTRL);
			val |= TX_CTRL_DUPLEX;
			bw32(bp, B44_TX_CTRL, val);
			netif_carrier_on(bp->dev);
			b44_link_report(bp);
		}
		return;
	}

Linus Torvalds's avatar
Linus Torvalds committed
	if (!b44_readphy(bp, MII_BMSR, &bmsr) &&
	    !b44_readphy(bp, B44_MII_AUXCTRL, &aux) &&
	    (bmsr != 0xffff)) {
		if (aux & MII_AUXCTRL_SPEED)
			bp->flags |= B44_FLAG_100_BASE_T;
		else
			bp->flags &= ~B44_FLAG_100_BASE_T;
		if (aux & MII_AUXCTRL_DUPLEX)
			bp->flags |= B44_FLAG_FULL_DUPLEX;
		else
			bp->flags &= ~B44_FLAG_FULL_DUPLEX;

		if (!netif_carrier_ok(bp->dev) &&
		    (bmsr & BMSR_LSTATUS)) {
			u32 val = br32(bp, B44_TX_CTRL);
			u32 local_adv, remote_adv;

			if (bp->flags & B44_FLAG_FULL_DUPLEX)
				val |= TX_CTRL_DUPLEX;
			else
				val &= ~TX_CTRL_DUPLEX;
			bw32(bp, B44_TX_CTRL, val);

			if (!(bp->flags & B44_FLAG_FORCE_LINK) &&
			    !b44_readphy(bp, MII_ADVERTISE, &local_adv) &&
			    !b44_readphy(bp, MII_LPA, &remote_adv))
				b44_set_flow_ctrl(bp, local_adv, remote_adv);

			/* Link now up */
			netif_carrier_on(bp->dev);
			b44_link_report(bp);
		} else if (netif_carrier_ok(bp->dev) && !(bmsr & BMSR_LSTATUS)) {
			/* Link now down */
			netif_carrier_off(bp->dev);
			b44_link_report(bp);
		}

		if (bmsr & BMSR_RFAULT)
			netdev_warn(bp->dev, "Remote fault detected in PHY\n");
Linus Torvalds's avatar
Linus Torvalds committed
		if (bmsr & BMSR_JCD)
			netdev_warn(bp->dev, "Jabber detected in PHY\n");
Linus Torvalds's avatar
Linus Torvalds committed
	}
}

static void b44_timer(unsigned long __opaque)
{
	struct b44 *bp = (struct b44 *) __opaque;

	spin_lock_irq(&bp->lock);

	b44_check_phy(bp);

	b44_stats_update(bp);

	spin_unlock_irq(&bp->lock);

	mod_timer(&bp->timer, round_jiffies(jiffies + HZ));
Linus Torvalds's avatar
Linus Torvalds committed
}

static void b44_tx(struct b44 *bp)
{
	u32 cur, cons;

	cur  = br32(bp, B44_DMATX_STAT) & DMATX_STAT_CDMASK;
	cur /= sizeof(struct dma_desc);

	/* XXX needs updating when NETIF_F_SG is supported */
	for (cons = bp->tx_cons; cons != cur; cons = NEXT_TX(cons)) {
		struct ring_info *rp = &bp->tx_buffers[cons];
		struct sk_buff *skb = rp->skb;

		BUG_ON(skb == NULL);
Linus Torvalds's avatar
Linus Torvalds committed

		dma_unmap_single(bp->sdev->dma_dev,
				 rp->mapping,
				 skb->len,
				 DMA_TO_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed
		rp->skb = NULL;
		dev_kfree_skb(skb);
Linus Torvalds's avatar
Linus Torvalds committed
	}

	bp->tx_cons = cons;
	if (netif_queue_stopped(bp->dev) &&
	    TX_BUFFS_AVAIL(bp) > B44_TX_WAKEUP_THRESH)
		netif_wake_queue(bp->dev);

	bw32(bp, B44_GPTIMER, 0);
}

/* Works like this.  This chip writes a 'struct rx_header" 30 bytes
 * before the DMA address you give it.  So we allocate 30 more bytes
 * for the RX buffer, DMA map all of it, skb_reserve the 30 bytes, then
 * point the chip at 30 bytes past where the rx_header will go.
 */
static int b44_alloc_rx_skb(struct b44 *bp, int src_idx, u32 dest_idx_unmasked)
{
	struct dma_desc *dp;
	struct ring_info *src_map, *map;
	struct rx_header *rh;
	struct sk_buff *skb;
	dma_addr_t mapping;
	int dest_idx;
	u32 ctrl;

	src_map = NULL;
	if (src_idx >= 0)
		src_map = &bp->rx_buffers[src_idx];
	dest_idx = dest_idx_unmasked & (B44_RX_RING_SIZE - 1);
	map = &bp->rx_buffers[dest_idx];
	skb = netdev_alloc_skb(bp->dev, RX_PKT_BUF_SZ);
Linus Torvalds's avatar
Linus Torvalds committed
	if (skb == NULL)
		return -ENOMEM;

	mapping = dma_map_single(bp->sdev->dma_dev, skb->data,
				 RX_PKT_BUF_SZ,
				 DMA_FROM_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed

	/* Hardware bug work-around, the chip is unable to do PCI DMA
	   to/from anything above 1GB :-( */
	if (dma_mapping_error(bp->sdev->dma_dev, mapping) ||
		mapping + RX_PKT_BUF_SZ > DMA_BIT_MASK(30)) {
Linus Torvalds's avatar
Linus Torvalds committed
		/* Sigh... */
		if (!dma_mapping_error(bp->sdev->dma_dev, mapping))
			dma_unmap_single(bp->sdev->dma_dev, mapping,
					     RX_PKT_BUF_SZ, DMA_FROM_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed
		dev_kfree_skb_any(skb);
		skb = __netdev_alloc_skb(bp->dev, RX_PKT_BUF_SZ, GFP_ATOMIC|GFP_DMA);
Linus Torvalds's avatar
Linus Torvalds committed
		if (skb == NULL)
			return -ENOMEM;
		mapping = dma_map_single(bp->sdev->dma_dev, skb->data,
					 RX_PKT_BUF_SZ,
					 DMA_FROM_DEVICE);
		if (dma_mapping_error(bp->sdev->dma_dev, mapping) ||
		    mapping + RX_PKT_BUF_SZ > DMA_BIT_MASK(30)) {
			if (!dma_mapping_error(bp->sdev->dma_dev, mapping))
				dma_unmap_single(bp->sdev->dma_dev, mapping, RX_PKT_BUF_SZ,DMA_FROM_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed
			dev_kfree_skb_any(skb);
			return -ENOMEM;
		}
		bp->force_copybreak = 1;
	rh = (struct rx_header *) skb->data;
Linus Torvalds's avatar
Linus Torvalds committed

	rh->len = 0;
	rh->flags = 0;

	map->skb = skb;
	map->mapping = mapping;
Linus Torvalds's avatar
Linus Torvalds committed

	if (src_map != NULL)
		src_map->skb = NULL;

	ctrl = (DESC_CTRL_LEN & RX_PKT_BUF_SZ);
Linus Torvalds's avatar
Linus Torvalds committed
	if (dest_idx == (B44_RX_RING_SIZE - 1))
		ctrl |= DESC_CTRL_EOT;

	dp = &bp->rx_ring[dest_idx];
	dp->ctrl = cpu_to_le32(ctrl);
	dp->addr = cpu_to_le32((u32) mapping + bp->dma_offset);
Linus Torvalds's avatar
Linus Torvalds committed

	if (bp->flags & B44_FLAG_RX_RING_HACK)
		b44_sync_dma_desc_for_device(bp->sdev, bp->rx_ring_dma,
			                    DMA_BIDIRECTIONAL);
Linus Torvalds's avatar
Linus Torvalds committed
	return RX_PKT_BUF_SZ;
}

static void b44_recycle_rx(struct b44 *bp, int src_idx, u32 dest_idx_unmasked)
{
	struct dma_desc *src_desc, *dest_desc;
	struct ring_info *src_map, *dest_map;
	struct rx_header *rh;
	int dest_idx;
Al Viro's avatar
Al Viro committed
	__le32 ctrl;
Linus Torvalds's avatar
Linus Torvalds committed

	dest_idx = dest_idx_unmasked & (B44_RX_RING_SIZE - 1);
	dest_desc = &bp->rx_ring[dest_idx];
	dest_map = &bp->rx_buffers[dest_idx];
	src_desc = &bp->rx_ring[src_idx];
	src_map = &bp->rx_buffers[src_idx];

	dest_map->skb = src_map->skb;
	rh = (struct rx_header *) src_map->skb->data;
	rh->len = 0;
	rh->flags = 0;
	dest_map->mapping = src_map->mapping;
Linus Torvalds's avatar
Linus Torvalds committed

	if (bp->flags & B44_FLAG_RX_RING_HACK)
		b44_sync_dma_desc_for_cpu(bp->sdev, bp->rx_ring_dma,
			                 src_idx * sizeof(*src_desc),
			                 DMA_BIDIRECTIONAL);
Linus Torvalds's avatar
Linus Torvalds committed
	ctrl = src_desc->ctrl;
	if (dest_idx == (B44_RX_RING_SIZE - 1))
		ctrl |= cpu_to_le32(DESC_CTRL_EOT);
	else
		ctrl &= cpu_to_le32(~DESC_CTRL_EOT);

	dest_desc->ctrl = ctrl;
	dest_desc->addr = src_desc->addr;
Linus Torvalds's avatar
Linus Torvalds committed
	src_map->skb = NULL;

	if (bp->flags & B44_FLAG_RX_RING_HACK)
		b44_sync_dma_desc_for_device(bp->sdev, bp->rx_ring_dma,
					     dest_idx * sizeof(*dest_desc),
					     DMA_BIDIRECTIONAL);
	dma_sync_single_for_device(bp->sdev->dma_dev, dest_map->mapping,
				   RX_PKT_BUF_SZ,
				   DMA_FROM_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed
}

static int b44_rx(struct b44 *bp, int budget)
{
	int received;
	u32 cons, prod;

	received = 0;
	prod  = br32(bp, B44_DMARX_STAT) & DMARX_STAT_CDMASK;
	prod /= sizeof(struct dma_desc);
	cons = bp->rx_cons;

	while (cons != prod && budget > 0) {
		struct ring_info *rp = &bp->rx_buffers[cons];
		struct sk_buff *skb = rp->skb;
		dma_addr_t map = rp->mapping;
Linus Torvalds's avatar
Linus Torvalds committed
		struct rx_header *rh;
		u16 len;

		dma_sync_single_for_cpu(bp->sdev->dma_dev, map,
					RX_PKT_BUF_SZ,
					DMA_FROM_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed
		rh = (struct rx_header *) skb->data;
Al Viro's avatar
Al Viro committed
		len = le16_to_cpu(rh->len);
		if ((len > (RX_PKT_BUF_SZ - RX_PKT_OFFSET)) ||
Linus Torvalds's avatar
Linus Torvalds committed
		    (rh->flags & cpu_to_le16(RX_FLAG_ERRORS))) {
		drop_it:
			b44_recycle_rx(bp, cons, bp->rx_prod);
		drop_it_no_recycle:
Linus Torvalds's avatar
Linus Torvalds committed
			goto next_pkt;
		}

		if (len == 0) {
			int i = 0;

			do {
				udelay(2);
				barrier();
Al Viro's avatar
Al Viro committed
				len = le16_to_cpu(rh->len);
Linus Torvalds's avatar
Linus Torvalds committed
			} while (len == 0 && i++ < 5);
			if (len == 0)
				goto drop_it;
		}

		/* Omit CRC. */
		len -= 4;

		if (!bp->force_copybreak && len > RX_COPY_THRESHOLD) {
Linus Torvalds's avatar
Linus Torvalds committed
			int skb_size;
			skb_size = b44_alloc_rx_skb(bp, cons, bp->rx_prod);
			if (skb_size < 0)
				goto drop_it;
			dma_unmap_single(bp->sdev->dma_dev, map,
					 skb_size, DMA_FROM_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed
			/* Leave out rx_header */
			skb_put(skb, len + RX_PKT_OFFSET);
			skb_pull(skb, RX_PKT_OFFSET);
Linus Torvalds's avatar
Linus Torvalds committed
		} else {
			struct sk_buff *copy_skb;

			b44_recycle_rx(bp, cons, bp->rx_prod);
			copy_skb = netdev_alloc_skb(bp->dev, len + 2);
Linus Torvalds's avatar
Linus Torvalds committed
			if (copy_skb == NULL)
				goto drop_it_no_recycle;

			skb_reserve(copy_skb, 2);
			skb_put(copy_skb, len);
			/* DMA sync done above, copy just the actual packet */
			skb_copy_from_linear_data_offset(skb, RX_PKT_OFFSET,
Linus Torvalds's avatar
Linus Torvalds committed
			skb = copy_skb;
		}
		skb_checksum_none_assert(skb);
Linus Torvalds's avatar
Linus Torvalds committed
		skb->protocol = eth_type_trans(skb, bp->dev);
		netif_receive_skb(skb);
		received++;
		budget--;
	next_pkt:
		bp->rx_prod = (bp->rx_prod + 1) &
			(B44_RX_RING_SIZE - 1);
		cons = (cons + 1) & (B44_RX_RING_SIZE - 1);
	}

	bp->rx_cons = cons;
	bw32(bp, B44_DMARX_PTR, cons * sizeof(struct dma_desc));

	return received;
}

static int b44_poll(struct napi_struct *napi, int budget)
Linus Torvalds's avatar
Linus Torvalds committed
{
	struct b44 *bp = container_of(napi, struct b44, napi);
	int work_done;
Linus Torvalds's avatar
Linus Torvalds committed

	spin_lock_irqsave(&bp->lock, flags);
Linus Torvalds's avatar
Linus Torvalds committed

	if (bp->istat & (ISTAT_TX | ISTAT_TO)) {
		/* spin_lock(&bp->tx_lock); */
		b44_tx(bp);
		/* spin_unlock(&bp->tx_lock); */
	}
	if (bp->istat & ISTAT_RFO) {	/* fast recovery, in ~20msec */
		bp->istat &= ~ISTAT_RFO;
		b44_disable_ints(bp);
		ssb_device_enable(bp->sdev, 0); /* resets ISTAT_RFO */
		b44_init_rings(bp);
		b44_init_hw(bp, B44_FULL_RESET_SKIP_PHY);
		netif_wake_queue(bp->dev);
	}

	spin_unlock_irqrestore(&bp->lock, flags);
Linus Torvalds's avatar
Linus Torvalds committed

	work_done = 0;
	if (bp->istat & ISTAT_RX)
		work_done += b44_rx(bp, budget);
Linus Torvalds's avatar
Linus Torvalds committed

	if (bp->istat & ISTAT_ERRORS) {
Linus Torvalds's avatar
Linus Torvalds committed
		b44_halt(bp);
		b44_init_rings(bp);
		b44_init_hw(bp, B44_FULL_RESET_SKIP_PHY);
Linus Torvalds's avatar
Linus Torvalds committed
		netif_wake_queue(bp->dev);
		spin_unlock_irqrestore(&bp->lock, flags);
		napi_complete(napi);
Linus Torvalds's avatar
Linus Torvalds committed
		b44_enable_ints(bp);
	}

static irqreturn_t b44_interrupt(int irq, void *dev_id)
Linus Torvalds's avatar
Linus Torvalds committed
{
	struct net_device *dev = dev_id;
	struct b44 *bp = netdev_priv(dev);
	u32 istat, imask;
	int handled = 0;

Linus Torvalds's avatar
Linus Torvalds committed

	istat = br32(bp, B44_ISTAT);
	imask = br32(bp, B44_IMASK);

	/* The interrupt mask register controls which interrupt bits
	 * will actually raise an interrupt to the CPU when set by hw/firmware,
	 * but doesn't mask off the bits.
Linus Torvalds's avatar
Linus Torvalds committed
	 */
	istat &= imask;
	if (istat) {
		handled = 1;

		if (unlikely(!netif_running(dev))) {
			netdev_info(dev, "late interrupt\n");
		if (napi_schedule_prep(&bp->napi)) {
Linus Torvalds's avatar
Linus Torvalds committed
			/* NOTE: These writes are posted by the readback of
			 *       the ISTAT register below.
			 */
			bp->istat = istat;
			__b44_disable_ints(bp);
			__napi_schedule(&bp->napi);
Linus Torvalds's avatar
Linus Torvalds committed
		bw32(bp, B44_ISTAT, istat);
		br32(bp, B44_ISTAT);
	}
Linus Torvalds's avatar
Linus Torvalds committed
	return IRQ_RETVAL(handled);
}

static void b44_tx_timeout(struct net_device *dev)
{
	struct b44 *bp = netdev_priv(dev);

	netdev_err(dev, "transmit timed out, resetting\n");
Linus Torvalds's avatar
Linus Torvalds committed

	spin_lock_irq(&bp->lock);

	b44_halt(bp);
	b44_init_rings(bp);
	b44_init_hw(bp, B44_FULL_RESET);
Linus Torvalds's avatar
Linus Torvalds committed

	spin_unlock_irq(&bp->lock);

	b44_enable_ints(bp);

	netif_wake_queue(dev);
}

static netdev_tx_t b44_start_xmit(struct sk_buff *skb, struct net_device *dev)
Linus Torvalds's avatar
Linus Torvalds committed
{
	struct b44 *bp = netdev_priv(dev);
Linus Torvalds's avatar
Linus Torvalds committed
	dma_addr_t mapping;
	u32 len, entry, ctrl;
Linus Torvalds's avatar
Linus Torvalds committed

	len = skb->len;
	spin_lock_irqsave(&bp->lock, flags);
Linus Torvalds's avatar
Linus Torvalds committed

	/* This is a hard error, log it. */
	if (unlikely(TX_BUFFS_AVAIL(bp) < 1)) {
		netif_stop_queue(dev);
		netdev_err(dev, "BUG! Tx Ring full when queue awake!\n");
	mapping = dma_map_single(bp->sdev->dma_dev, skb->data, len, DMA_TO_DEVICE);
	if (dma_mapping_error(bp->sdev->dma_dev, mapping) || mapping + len > DMA_BIT_MASK(30)) {
		struct sk_buff *bounce_skb;

Linus Torvalds's avatar
Linus Torvalds committed
		/* Chip can't handle DMA to/from >1GB, use bounce buffer */
		if (!dma_mapping_error(bp->sdev->dma_dev, mapping))
			dma_unmap_single(bp->sdev->dma_dev, mapping, len,
Linus Torvalds's avatar
Linus Torvalds committed

		bounce_skb = __netdev_alloc_skb(dev, len, GFP_ATOMIC | GFP_DMA);
Linus Torvalds's avatar
Linus Torvalds committed
		if (!bounce_skb)
Linus Torvalds's avatar
Linus Torvalds committed

		mapping = dma_map_single(bp->sdev->dma_dev, bounce_skb->data,
					 len, DMA_TO_DEVICE);
		if (dma_mapping_error(bp->sdev->dma_dev, mapping) || mapping + len > DMA_BIT_MASK(30)) {
			if (!dma_mapping_error(bp->sdev->dma_dev, mapping))
				dma_unmap_single(bp->sdev->dma_dev, mapping,
						     len, DMA_TO_DEVICE);
Linus Torvalds's avatar
Linus Torvalds committed
			dev_kfree_skb_any(bounce_skb);
		skb_copy_from_linear_data(skb, skb_put(bounce_skb, len), len);
Linus Torvalds's avatar
Linus Torvalds committed
		dev_kfree_skb_any(skb);
		skb = bounce_skb;
	}

	entry = bp->tx_prod;
	bp->tx_buffers[entry].skb = skb;
	bp->tx_buffers[entry].mapping = mapping;
Linus Torvalds's avatar
Linus Torvalds committed

	ctrl  = (len & DESC_CTRL_LEN);
	ctrl |= DESC_CTRL_IOC | DESC_CTRL_SOF | DESC_CTRL_EOF;
	if (entry == (B44_TX_RING_SIZE - 1))
		ctrl |= DESC_CTRL_EOT;

	bp->tx_ring[entry].ctrl = cpu_to_le32(ctrl);
	bp->tx_ring[entry].addr = cpu_to_le32((u32) mapping+bp->dma_offset);