/**
* Airgo MIMO wireless driver
*
* Copyright (c) 2007 Li YanBo <dreamfly281@gmail.com>
* Thanks for Jeff Williams <angelbane@gmail.com> do reverse engineer
* works and published the SPECS at http://airgo.wdwconsulting.net/mymoin
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/init.h>
#include <linux/etherdevice.h>
#include <linux/pci.h>
#include <linux/delay.h>
#include "agnx.h"
#include "debug.h"
#include "xmit.h"
#include "phy.h"
MODULE_AUTHOR("Li YanBo <dreamfly281@gmail.com>");
MODULE_DESCRIPTION("Airgo MIMO PCI wireless driver");
MODULE_LICENSE("GPL");
static struct pci_device_id agnx_pci_id_tbl[] __devinitdata = {
{ PCI_DEVICE(0x17cb, 0x0001) }, /* Beklin F5d8010, Netgear WGM511 etc */
{ PCI_DEVICE(0x17cb, 0x0002) }, /* Netgear Wpnt511 */
{ 0 }
};
MODULE_DEVICE_TABLE(pci, agnx_pci_id_tbl);
static inline void agnx_interrupt_ack(struct agnx_priv *priv, u32 *reason)
{
void __iomem *ctl = priv->ctl;
u32 reg;
if (*reason & AGNX_STAT_RX) {
/* Mark complete RX */
reg = ioread32(ctl + AGNX_CIR_RXCTL);
reg |= 0x4;
iowrite32(reg, ctl + AGNX_CIR_RXCTL);
/* disable Rx interrupt */
}
if (*reason & AGNX_STAT_TX) {
reg = ioread32(ctl + AGNX_CIR_TXDCTL);
if (reg & 0x4) {
iowrite32(reg, ctl + AGNX_CIR_TXDCTL);
*reason |= AGNX_STAT_TXD;
}
reg = ioread32(ctl + AGNX_CIR_TXMCTL);
if (reg & 0x4) {
iowrite32(reg, ctl + AGNX_CIR_TXMCTL);
*reason |= AGNX_STAT_TXM;
}
}
#if 0
if (*reason & AGNX_STAT_X) {
reg = ioread32(ctl + AGNX_INT_STAT);
iowrite32(reg, ctl + AGNX_INT_STAT);
/* FIXME reinit interrupt mask */
reg = 0xc390bf9 & ~IRQ_TX_BEACON;
reg &= ~IRQ_TX_DISABLE;
iowrite32(reg, ctl + AGNX_INT_MASK);
iowrite32(0x800, ctl + AGNX_CIR_BLKCTL);
}
#endif
} /* agnx_interrupt_ack */
static irqreturn_t agnx_interrupt_handler(int irq, void *dev_id)
{
struct ieee80211_hw *dev = dev_id;
struct agnx_priv *priv = dev->priv;
void __iomem *ctl = priv->ctl;
irqreturn_t ret = IRQ_NONE;
u32 irq_reason;
spin_lock(&priv->lock);
/* printk(KERN_ERR PFX "Get a interrupt %s\n", __func__); */
if (priv->init_status != AGNX_START)
goto out;
/* FiXME Here has no lock, Is this will lead to race? */
irq_reason = ioread32(ctl + AGNX_CIR_BLKCTL);
if (!(irq_reason & 0x7))
goto out;
ret = IRQ_HANDLED;
priv->irq_status = ioread32(ctl + AGNX_INT_STAT);
/* printk(PFX "Interrupt reason is 0x%x\n", irq_reason); */
/* Make sure the txm and txd flags don't conflict with other unknown
interrupt flag, maybe is not necessary */
irq_reason &= 0xF;
disable_rx_interrupt(priv);
/* TODO Make sure the card finished initialized */
agnx_interrupt_ack(priv, &irq_reason);
if (irq_reason & AGNX_STAT_RX)
handle_rx_irq(priv);
if (irq_reason & AGNX_STAT_TXD)
handle_txd_irq(priv);
if (irq_reason & AGNX_STAT_TXM)
handle_txm_irq(priv);
if (irq_reason & AGNX_STAT_X)
handle_other_irq(priv);
enable_rx_interrupt(priv);
out:
spin_unlock(&priv->lock);
return ret;
} /* agnx_interrupt_handler */
/* FIXME */
static int agnx_tx(struct ieee80211_hw *dev, struct sk_buff *skb)
{
AGNX_TRACE;
return _agnx_tx(dev->priv, skb);
} /* agnx_tx */
static int agnx_get_mac_address(struct agnx_priv *priv)
{
void __iomem *ctl = priv->ctl;
u32 reg;
AGNX_TRACE;
/* Attention! directly read the MAC or other date from EEPROM will
lead to cardbus(WGM511) lock up when write to PM PLL register */
reg = agnx_read32(ctl, 0x3544);
udelay(40);
reg = agnx_read32(ctl, 0x354c);
udelay(50);
/* Get the mac address */
reg = agnx_read32(ctl, 0x3544);
udelay(40);
/* HACK */
reg = cpu_to_le32(reg);
priv->mac_addr[0] = ((u8 *)®)[2];
priv->mac_addr[1] = ((u8 *)®)[3];
reg = agnx_read32(ctl, 0x3548);
udelay(50);
*((u32 *)(priv->mac_addr + 2)) = cpu_to_le32(reg);
if (!is_valid_ether_addr(priv->mac_addr)) {
printk(KERN_WARNING PFX "read mac %pM\n", priv->mac_addr);
printk(KERN_WARNING PFX "Invalid hwaddr! Using random hwaddr\n");
random_ether_addr(priv->mac_addr);
}
return 0;
} /* agnx_get_mac_address */
static int agnx_alloc_rings(struct agnx_priv *priv)
{
unsigned int len;
AGNX_TRACE;
/* Allocate RX/TXM/TXD rings info */
priv->rx.size = AGNX_RX_RING_SIZE;
priv->txm.size = AGNX_TXM_RING_SIZE;
priv->txd.size = AGNX_TXD_RING_SIZE;
len = priv->rx.size + priv->txm.size + priv->txd.size;
/* priv->rx.info = kzalloc(sizeof(struct agnx_info) * len, GFP_KERNEL); */
priv->rx.info = kzalloc(sizeof(struct agnx_info) * len, GFP_ATOMIC);
if (!priv->rx.info)
return -ENOMEM;
priv->txm.info = priv->rx.info + priv->rx.size;
priv->txd.info = priv->txm.info + priv->txm.size;
/* Allocate RX/TXM/TXD descriptors */
priv->rx.desc = pci_alloc_consistent(priv->pdev, sizeof(struct agnx_desc) * len,
&priv->rx.dma);
if (!priv->rx.desc) {
kfree(priv->rx.info);
return -ENOMEM;
}
priv->txm.desc = priv->rx.desc + priv->rx.size;
priv->txm.dma = priv->rx.dma + sizeof(struct agnx_desc) * priv->rx.size;
priv->txd.desc = priv->txm.desc + priv->txm.size;
priv->txd.dma = priv->txm.dma + sizeof(struct agnx_desc) * priv->txm.size;
return 0;
} /* agnx_alloc_rings */
static void rings_free(struct agnx_priv *priv)
{
unsigned int len = priv->rx.size + priv->txm.size + priv->txd.size;
unsigned long flags;
AGNX_TRACE;
spin_lock_irqsave(&priv->lock, flags);
kfree(priv->rx.info);
pci_free_consistent(priv->pdev, sizeof(struct agnx_desc) * len,
priv->rx.desc, priv->rx.dma);
spin_unlock_irqrestore(&priv->lock, flags);
}
#if 0
static void agnx_periodic_work_handler(struct work_struct *work)
{
struct agnx_priv *priv = container_of(work, struct agnx_priv, periodic_work.work);
/* unsigned long flags; */
unsigned long delay;
/* fixme: using mutex?? */
/* spin_lock_irqsave(&priv->lock, flags); */
/* TODO Recalibrate*/
/* calibrate_oscillator(priv); */
/* antenna_calibrate(priv); */
/* agnx_send_packet(priv, 997); */
/* FIXME */
/* if (debug == 3) */
/* delay = msecs_to_jiffies(AGNX_PERIODIC_DELAY); */
/* else */
delay = msecs_to_jiffies(AGNX_PERIODIC_DELAY);
/* delay = round_jiffies(HZ * 15); */
queue_delayed_work(priv->hw->workqueue, &priv->periodic_work, delay);
/* spin_unlock_irqrestore(&priv->lock, flags); */
}
#endif
static int agnx_start(struct ieee80211_hw *dev)
{
struct agnx_priv *priv = dev->priv;
/* unsigned long delay; */
int err = 0;
AGNX_TRACE;
err = agnx_alloc_rings(priv);
if (err) {
printk(KERN_ERR PFX "Can't alloc RX/TXM/TXD rings\n");
goto out;
}
err = request_irq(priv->pdev->irq, &agnx_interrupt_handler,
IRQF_SHARED, "agnx_pci", dev);
if (err) {
printk(KERN_ERR PFX "Failed to register IRQ handler\n");
rings_free(priv);
goto out;
}
/* mdelay(500); */
might_sleep();
agnx_hw_init(priv);
/* mdelay(500); */
might_sleep();
priv->init_status = AGNX_START;
/* INIT_DELAYED_WORK(&priv->periodic_work, agnx_periodic_work_handler); */
/* delay = msecs_to_jiffies(AGNX_PERIODIC_DELAY); */
/* queue_delayed_work(priv->hw->workqueue, &priv->periodic_work, delay); */
out:
return err;
} /* agnx_start */
static void agnx_stop(struct ieee80211_hw *dev)
{
struct agnx_priv *priv = dev->priv;
AGNX_TRACE;
priv->init_status = AGNX_STOP;
/* make sure hardware will not generate irq */
agnx_hw_reset(priv);
free_irq(priv->pdev->irq, dev);
/* flush_workqueue(priv->hw->workqueue); */
/* cancel_delayed_work_sync(&priv->periodic_work); */
unfill_rings(priv);
rings_free(priv);
}
static int agnx_config(struct ieee80211_hw *dev, u32 changed)
{
struct agnx_priv *priv = dev->priv;
struct ieee80211_conf *conf = &dev->conf;
int channel = ieee80211_frequency_to_channel(conf->channel->center_freq);
AGNX_TRACE;
spin_lock(&priv->lock);
/* FIXME need priv lock? */
if (channel != priv->channel) {
priv->channel = channel;
agnx_set_channel(priv, priv->channel);
}
spin_unlock(&priv->lock);
return 0;
}
static void agnx_bss_info_changed(struct ieee80211_hw *dev,
struct ieee80211_vif *vif,
struct ieee80211_bss_conf *conf,
u32 changed)
{
struct agnx_priv *priv = dev->priv;
void __iomem *ctl = priv->ctl;
AGNX_TRACE;
if (!(changed & BSS_CHANGED_BSSID))
return;
spin_lock(&priv->lock);
if (memcmp(conf->bssid, priv->bssid, ETH_ALEN)) {
agnx_set_bssid(priv, conf->bssid);
memcpy(priv->bssid, conf->bssid, ETH_ALEN);
hash_write(priv, conf->bssid, BSSID_STAID);
sta_init(priv, BSSID_STAID);
/* FIXME needed? */
sta_power_init(priv, BSSID_STAID);
agnx_write32(ctl, AGNX_BM_MTSM, 0xff & ~0x1);
}
spin_unlock(&priv->lock);
} /* agnx_bss_info_changed */
static void agnx_configure_filter(struct ieee80211_hw *dev,
unsigned int changed_flags,
unsigned int *total_flags,
int mc_count, struct dev_mc_list *mclist)
{
unsigned int new_flags = 0;
*total_flags = new_flags;
/* TODO */
}
static int agnx_add_interface(struct ieee80211_hw *dev,
struct ieee80211_if_init_conf *conf)
{
struct agnx_priv *priv = dev->priv;
AGNX_TRACE;
spin_lock(&priv->lock);
/* FIXME */
if (priv->mode != NL80211_IFTYPE_MONITOR)
return -EOPNOTSUPP;
switch (conf->type) {
case NL80211_IFTYPE_STATION:
priv->mode = conf->type;
break;
default:
return -EOPNOTSUPP;
}
spin_unlock(&priv->lock);
return 0;
}
static void agnx_remove_interface(struct ieee80211_hw *dev,
struct ieee80211_if_init_conf *conf)
{
struct agnx_priv *priv = dev->priv;
AGNX_TRACE;
/* TODO */
priv->mode = NL80211_IFTYPE_MONITOR;
}
static int agnx_get_stats(struct ieee80211_hw *dev,
struct ieee80211_low_level_stats *stats)
{
struct agnx_priv *priv = dev->priv;
AGNX_TRACE;
spin_lock(&priv->lock);
/* TODO !! */
memcpy(stats, &priv->stats, sizeof(*stats));
spin_unlock(&priv->lock);
return 0;
}
static u64 agnx_get_tsft(struct ieee80211_hw *dev)
{
void __iomem *ctl = ((struct agnx_priv *)dev->priv)->ctl;
u32 tsftl;
u64 tsft;
AGNX_TRACE;
/* FIXME */
tsftl = ioread32(ctl + AGNX_TXM_TIMESTAMPLO);
tsft = ioread32(ctl + AGNX_TXM_TIMESTAMPHI);
tsft <<= 32;
tsft |= tsftl;
return tsft;
}
static int agnx_get_tx_stats(struct ieee80211_hw *dev,
struct ieee80211_tx_queue_stats *stats)
{
struct agnx_priv *priv = dev->priv;
AGNX_TRACE;
/* FIXME now we just using txd queue, but should using txm queue too */
stats[0].len = (priv->txd.idx - priv->txd.idx_sent) / 2;
stats[0].limit = priv->txd.size - 2;
stats[0].count = priv->txd.idx / 2;
return 0;
}
static struct ieee80211_ops agnx_ops = {
.tx = agnx_tx,
.start = agnx_start,
.stop = agnx_stop,
.add_interface = agnx_add_interface,
.remove_interface = agnx_remove_interface,
.config = agnx_config,
.bss_info_changed = agnx_bss_info_changed,
.configure_filter = agnx_configure_filter,
.get_stats = agnx_get_stats,
.get_tx_stats = agnx_get_tx_stats,
.get_tsf = agnx_get_tsft
};
static void __devexit agnx_pci_remove(struct pci_dev *pdev)
{
struct ieee80211_hw *dev = pci_get_drvdata(pdev);
struct agnx_priv *priv;
AGNX_TRACE;
if (!dev)
return;
priv = dev->priv;
ieee80211_unregister_hw(dev);
pci_iounmap(pdev, priv->ctl);
pci_iounmap(pdev, priv->data);
pci_release_regions(pdev);
pci_disable_device(pdev);
ieee80211_free_hw(dev);
}
static int __devinit agnx_pci_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct ieee80211_hw *dev;
struct agnx_priv *priv;
int err;
err = pci_enable_device(pdev);
if (err) {
dev_err(&pdev->dev, "can't enable pci device\n");
return err;
}
err = pci_request_regions(pdev, "agnx-pci");
if (err) {
dev_err(&pdev->dev, "can't reserve PCI resources\n");
return err;
}
if (pci_set_dma_mask(pdev, DMA_BIT_MASK(32)) ||
pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(32))) {
dev_err(&pdev->dev, "no suitable DMA available\n");
err = -EIO;
goto err_free_reg;
}
pci_set_master(pdev);
dev = ieee80211_alloc_hw(sizeof(*priv), &agnx_ops);
if (!dev) {
dev_err(&pdev->dev, "ieee80211 alloc failed\n");
err = -ENOMEM;
goto err_free_reg;
}
priv = dev->priv;
memset(priv, 0, sizeof(*priv));
priv->mode = NL80211_IFTYPE_MONITOR;
priv->pdev = pdev;
priv->hw = dev;
spin_lock_init(&priv->lock);
priv->init_status = AGNX_UNINIT;
priv->ctl = pci_iomap(pdev, 0, 0);
/* dev_dbg(&pdev->dev, "MEM1 mapped address is 0x%p\n", priv->ctl); */
if (!priv->ctl) {
dev_err(&pdev->dev, "can't map device memory\n");
err = -ENOMEM;
goto err_free_dev;
}
priv->data = pci_iomap(pdev, 1, 0);
if (!priv->data) {
dev_err(&pdev->dev, "can't map device memory\n");
err = -ENOMEM;
goto err_iounmap2;
}
pci_read_config_byte(pdev, PCI_REVISION_ID, &priv->revid);
priv->band.channels = (struct ieee80211_channel *)agnx_channels;
priv->band.n_channels = ARRAY_SIZE(agnx_channels);
priv->band.bitrates = (struct ieee80211_rate *)agnx_rates_80211g;
priv->band.n_bitrates = ARRAY_SIZE(agnx_rates_80211g);
/* Init ieee802.11 dev */
SET_IEEE80211_DEV(dev, &pdev->dev);
pci_set_drvdata(pdev, dev);
dev->extra_tx_headroom = sizeof(struct agnx_hdr);
/* FIXME It only include FCS in promious mode but not manage mode */
/* dev->flags = IEEE80211_HW_RX_INCLUDES_FCS; */
dev->channel_change_time = 5000;
dev->max_signal = 100;
/* FIXME */
dev->queues = 1;
agnx_get_mac_address(priv);
SET_IEEE80211_PERM_ADDR(dev, priv->mac_addr);
/* /\* FIXME *\/ */
/* for (i = 1; i < NUM_DRIVE_MODES; i++) { */
/* err = ieee80211_register_hwmode(dev, &priv->modes[i]); */
/* if (err) { */
/* printk(KERN_ERR PFX "Can't register hwmode\n"); */
/* goto err_iounmap; */
/* } */
/* } */
priv->channel = 1;
dev->wiphy->bands[IEEE80211_BAND_2GHZ] = &priv->band;
err = ieee80211_register_hw(dev);
if (err) {
dev_err(&pdev->dev, "can't register hardware\n");
goto err_iounmap;
}
agnx_hw_reset(priv);
dev_info(&pdev->dev, "%s: hwaddr %pM, Rev 0x%02x\n",
wiphy_name(dev->wiphy),
dev->wiphy->perm_addr, priv->revid);
return 0;
err_iounmap:
pci_iounmap(pdev, priv->data);
err_iounmap2:
pci_iounmap(pdev, priv->ctl);
err_free_dev:
pci_set_drvdata(pdev, NULL);
ieee80211_free_hw(dev);
err_free_reg:
pci_release_regions(pdev);
pci_disable_device(pdev);
return err;
} /* agnx_pci_probe*/
#ifdef CONFIG_PM
static int agnx_pci_suspend(struct pci_dev *pdev, pm_message_t state)
{
struct ieee80211_hw *dev = pci_get_drvdata(pdev);
AGNX_TRACE;
ieee80211_stop_queues(dev);
agnx_stop(dev);
pci_save_state(pdev);
pci_set_power_state(pdev, pci_choose_state(pdev, state));
return 0;
}
static int agnx_pci_resume(struct pci_dev *pdev)
{
struct ieee80211_hw *dev = pci_get_drvdata(pdev);
AGNX_TRACE;
pci_set_power_state(pdev, PCI_D0);
pci_restore_state(pdev);
agnx_start(dev);
ieee80211_wake_queues(dev);
return 0;
}
#else
#define agnx_pci_suspend NULL
#define agnx_pci_resume NULL
#endif /* CONFIG_PM */
static struct pci_driver agnx_pci_driver = {
.name = "agnx-pci",
.id_table = agnx_pci_id_tbl,
.probe = agnx_pci_probe,
.remove = __devexit_p(agnx_pci_remove),
.suspend = agnx_pci_suspend,
.resume = agnx_pci_resume,
};
static int __init agnx_pci_init(void)
{
AGNX_TRACE;
return pci_register_driver(&agnx_pci_driver);
}
static void __exit agnx_pci_exit(void)
{
AGNX_TRACE;
pci_unregister_driver(&agnx_pci_driver);
}
module_init(agnx_pci_init);
module_exit(agnx_pci_exit);