From 6eeafd614e36021635d37840f4bfaa5f34395e34 Mon Sep 17 00:00:00 2001 From: markster Date: Wed, 7 Nov 2001 17:47:29 +0000 Subject: Version 0.1.0 from FTP git-svn-id: http://svn.digium.com/svn/zaptel/trunk@22 5390a7c7-147a-4af0-8ec9-7488f05a26cb --- wcfxo.c | 734 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 734 insertions(+) create mode 100755 wcfxo.c (limited to 'wcfxo.c') diff --git a/wcfxo.c b/wcfxo.c new file mode 100755 index 0000000..3c0785a --- /dev/null +++ b/wcfxo.c @@ -0,0 +1,734 @@ +/* + * Wilcard X100P FXO Interface Driver for Zapata Telephony interface + * + * Written by Mark Spencer + * Matthew Fredrickson + * + * Copyright (C) 2001, Linux Support Services, Inc. + * + * All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#ifdef STANDALONE_ZAPATA +#include "zaptel.h" +#else +#include +#endif + +#define WC_MAX_IFACES 128 + +#define WC_CNTL 0x00 +#define WC_OPER 0x01 +#define WC_AUXC 0x02 +#define WC_AUXD 0x03 +#define WC_MASK0 0x04 +#define WC_MASK1 0x05 +#define WC_INTSTAT 0x06 + +#define WC_DMAWS 0x08 +#define WC_DMAWI 0x0c +#define WC_DMAWE 0x10 +#define WC_DMARS 0x18 +#define WC_DMARI 0x1c +#define WC_DMARE 0x20 + +#define WC_AUXFUNC 0x2b +#define WC_SERCTL 0x2d +#define WC_FSCDELAY 0x2f + +#define FLAG_EMPTY 0 +#define FLAG_WRITE 1 +#define FLAG_READ 2 + +#define RING_DEBOUNCE 64 /* Ringer Debounce (in ms) */ +#define BATT_DEBOUNCE 8 /* Battery debounce (in ms) */ + +struct reg { + int flags; + unsigned char index; + unsigned char reg; + unsigned char value; +}; + +static int wecareregs[] = +{ 5, 6, 9, 11, 12, 13, 17, 19, }; + +struct wcfxo { + struct pci_dev *dev; + char *variety; + struct zt_span span; + struct zt_chan chan; + int usecount; + int dead; + int pos; + int flags; + int freeregion; + int ring; + int battery; + int wregcount; + int readpos; + int rreadpos; + int ringdebounce; + int battdebounce; + int allread; + int regoffset; /* How far off our registers are from what we expect */ + int alt; + int ignoreread; + int reset; + /* Up to 6 register can be written at a time */ + struct reg regs[ZT_CHUNKSIZE]; + struct reg midregs[ZT_CHUNKSIZE]; + struct reg oldregs[ZT_CHUNKSIZE]; + /* Up to 32 registers of whatever we most recently read */ + unsigned char readregs[32]; + unsigned long ioaddr; + dma_addr_t readdma; + dma_addr_t writedma; + volatile int *writechunk; /* Double-word aligned write memory */ + volatile int *readchunk; /* Double-word aligned read memory */ +}; + +#define FLAG_INVERTSER (1 << 0) +#define FLAG_USE_XTAL (1 << 1) +#define FLAG_DOUBLE_CLOCK (1 << 2) +#define FLAG_RESET_ON_AUX5 (1 << 3) + +struct wcfxo_desc { + char *name; + int flags; +}; + +static struct wcfxo_desc wcfxo = { "Wildcard Prototype", 0}; +static struct wcfxo_desc wcx100p = { "Wildcard X100P", + FLAG_INVERTSER | FLAG_USE_XTAL | FLAG_DOUBLE_CLOCK }; + +static struct wcfxo *ifaces[WC_MAX_IFACES]; + +static void wcfxo_release(struct wcfxo *wc); + +static int debug = 0; + +static inline void wcfxo_transmitprep(struct wcfxo *wc, unsigned char ints) +{ + volatile int *writechunk; + int x; + int written=0; + unsigned short cmd; + if (ints & 0x01) + /* Write is at interrupt address. Start writing from normal offset */ + writechunk = wc->writechunk; + else + writechunk = wc->writechunk + ZT_CHUNKSIZE * 2; + /* Calculate Transmission */ + zt_transmit(&wc->span); + + for (x=0;xflags & FLAG_INVERTSER) + writechunk[x << 1] = + ~((unsigned short)(zt_mulaw[wc->chan.writechunk[x]])| 0x1) << 16; + else + writechunk[x << 1] = + ((unsigned short)(zt_mulaw[wc->chan.writechunk[x]])| 0x1) << 16; + + /* We always have a command to follow our signal */ + if (!wc->regs[x].flags) { + /* Fill in an empty register command with a read for a potentially useful register */ + wc->regs[x].flags = FLAG_READ; + wc->regs[x].reg = wecareregs[wc->readpos]; + wc->regs[x].index = wc->readpos; + wc->readpos++; + if (wc->readpos >= (sizeof(wecareregs) / sizeof(wecareregs[0]))) { + wc->allread = 1; + wc->readpos = 0; + } + } + + /* Prepare the command to follow it */ + switch(wc->regs[x].flags) { + case FLAG_READ: + cmd = (wc->regs[x].reg | 0x20) << 8; + break; + case FLAG_WRITE: + cmd = (wc->regs[x].reg << 8) | (wc->regs[x].value & 0xff); + written = 1; + /* Wait at least four samples before reading */ + wc->ignoreread = 4; + break; + default: + printk("wcfxo: Huh? No read or write??\n"); + cmd = 0; + } + /* Setup the write chunk */ + if (wc->flags & FLAG_INVERTSER) + writechunk[(x << 1) + 1] = ~(cmd << 16); + else + writechunk[(x << 1) + 1] = cmd << 16; + } + if (written) + wc->readpos = 0; + wc->wregcount = 0; + +} + +static inline void wcfxo_receiveprep(struct wcfxo *wc, unsigned char ints) +{ + volatile int *readchunk; + int x; + int realreg; + int realval; + if (ints & 0x08) + /* Read is at interrupt address. Valid data is available at normal offset */ + readchunk = wc->readchunk; + else + readchunk = wc->readchunk + ZT_CHUNKSIZE * 2; + for (x=0;xoldregs[x].flags == FLAG_READ && !wc->ignoreread) { + realreg = wecareregs[(wc->regs[x].index + wc->regoffset) % + (sizeof(wecareregs) / sizeof(wecareregs[0]))]; + realval = (readchunk[(x << 1) +wc->alt] >> 16) & 0xff; + if ((realval == 0x89) && (realreg != 0x9)) { + /* Some sort of slippage, correct for it */ + while(realreg != 0x9) { + /* Find register 9 */ + realreg = wecareregs[(wc->regs[x].index + ++wc->regoffset) % + (sizeof(wecareregs) / sizeof(wecareregs[0]))]; + wc->regoffset = wc->regoffset % (sizeof(wecareregs) / sizeof(wecareregs[0])); + } + if (debug) + printk("New regoffset: %d\n", wc->regoffset); + } + /* Receive into the proper register */ + wc->readregs[realreg] = realval; + } + wc->chan.readchunk[x] = zt_lin2mu[((short)(readchunk[(x << 1) + (1 - wc->alt)] >> 16)) + 32768]; + } + for (x=0;xoldregs[x] = wc->midregs[x]; + wc->midregs[x] = wc->regs[x]; + wc->regs[x].flags = FLAG_EMPTY; + } + if (wc->ignoreread) + wc->ignoreread--; + zt_receive(&wc->span); +} + +static void wcfxo_interrupt(int irq, void *dev_id, struct pt_regs *regs) +{ + struct wcfxo *wc = dev_id; + unsigned char ints; + unsigned char b; + + ints = inb(wc->ioaddr + WC_INTSTAT); + outb(ints, wc->ioaddr + WC_INTSTAT); + + if (ints & 0x0f) { + wcfxo_transmitprep(wc, ints); + wcfxo_receiveprep(wc, ints); + } + + if (ints & 0x10) + printk("PCI Master abort\n"); + + if (ints & 0x20) + printk("PCI Target abort\n"); + + if (1 /* !(wc->report % 0xf) */) { + /* Check RING from register and debounce for 8ms */ + b = wc->readregs[0x5] & 0x60; + if (!b) { + if (wc->ring && !wc->ringdebounce) { + if (wc->ring == 2) { + if (debug) + printk("NO RING!\n"); + zt_hooksig(&wc->chan, ZT_RXSIG_OFFHOOK); + } + wc->ring = 0; + wc->ringdebounce = RING_DEBOUNCE; + } + } else { + /* RING */ + if ((wc->ring < 2) && !wc->ringdebounce) { + if (wc->ring == 1) { + /* It's read as ringing for at least 8ms */ + if (debug) + printk("RING!\n"); + zt_hooksig(&wc->chan, ZT_RXSIG_RING); + wc->ring = 2; + } else { + /* Ooh, we saw a ring, now lets see if it hangs around + or is just someone plugging the phone in or something + along those lines */ + wc->ring = 1; + } + wc->ringdebounce = RING_DEBOUNCE; + } else if (wc->ring == 2) { /* Reset debounce if we're ringing */ + wc->ringdebounce = RING_DEBOUNCE; + } +#if 0 + else if (wc->ring == 1) { + printk("Ring1: Debounce: %d\n", wc->ringdebounce); + } +#endif + } + + /* Check for BATTERY from register and debounce for 8 ms */ + b = wc->readregs[0xc] & 0xf; + if (!b) { +#if 0 + if (wc->battery) + printk("Battery loss: %d (%d debounce)\n", b, wc->battdebounce); +#endif + if (wc->battery && !wc->battdebounce) { + if (debug) + printk("NO BATTERY!\n"); + wc->battery = 0; + zt_hooksig(&wc->chan, ZT_RXSIG_ONHOOK); + wc->battdebounce = BATT_DEBOUNCE; + } else if (!wc->battery) + wc->battdebounce = BATT_DEBOUNCE; + } else if (b == 0xf) { + if (!wc->battery && !wc->battdebounce) { + if (debug) + printk("BATTERY!\n"); + zt_hooksig(&wc->chan, ZT_RXSIG_OFFHOOK); + wc->battery = 1; + wc->battdebounce = BATT_DEBOUNCE; + } else if (wc->battery) + wc->battdebounce = BATT_DEBOUNCE; + } else { + /* It's something else... */ + wc->battdebounce = BATT_DEBOUNCE; + } + + if (wc->ringdebounce) + wc->ringdebounce--; + if (wc->battdebounce) + wc->battdebounce--; + } +} + +static int wcfxo_setreg(struct wcfxo *wc, unsigned char reg, unsigned char value) +{ + int x; + if (wc->wregcount < ZT_CHUNKSIZE) { + x = wc->wregcount; + wc->regs[x].reg = reg; + wc->regs[x].value = value; + wc->regs[x].flags = FLAG_WRITE; + wc->wregcount++; + return 0; + } + printk("wcfxo: Out of space to write register %02x with %02x\n", reg, value); + return -1; +} + +static int wcfxo_open(struct zt_chan *chan) +{ + struct wcfxo *wc = chan->pvt; + if (wc->dead) + return -ENODEV; + wc->usecount++; + MOD_INC_USE_COUNT; + return 0; +} + +static int wcfxo_close(struct zt_chan *chan) +{ + struct wcfxo *wc = chan->pvt; + wc->usecount--; + MOD_DEC_USE_COUNT; + /* If we're dead, release us now */ + if (!wc->usecount && wc->dead) + wcfxo_release(wc); + return 0; +} + +static int wcfxo_hooksig(struct zt_chan *chan, zt_txsig_t txsig) +{ + struct wcfxo *wc = chan->pvt; + int reg=0; + switch(txsig) { + case ZT_TXSIG_START: + case ZT_TXSIG_OFFHOOK: + /* Take off hook and enable normal mode reception. This must + be done in two steps because of a hardware bug. */ + reg = wc->readregs[0x5] & ~0x08; + wcfxo_setreg(wc, 0x5, reg); + + reg = reg | 0x1; + wcfxo_setreg(wc, 0x5, reg); + break; + case ZT_TXSIG_ONHOOK: + /* Put on hook and enable on hook line monitor */ + reg = wc->readregs[0x5] & 0xfe; + wcfxo_setreg(wc, 0x5, reg); + + reg = reg | 0x08; + wcfxo_setreg(wc, 0x5, reg); + break; + default: + printk("wcfxo: Can't set tx state to %d\n", txsig); + } + if (debug) + printk("Setting hook state to %d (%02x)\n", txsig, reg); + return 0; +} + +static int wcfxo_initialize(struct wcfxo *wc) +{ + /* Zapata stuff */ + sprintf(wc->span.name, "TjModem/%d", wc->pos); + sprintf(wc->span.desc, "%s Board %d\n", wc->variety, wc->pos + 1); + sprintf(wc->chan.name, "TjModem/%d/%d", wc->pos, 0); + wc->chan.sigcap = ZT_SIG_FXSKS | ZT_SIG_FXSLS; + wc->chan.chanpos = 1; + wc->span.chans = &wc->chan; + wc->span.channels = 1; + wc->span.hooksig = wcfxo_hooksig; + wc->span.open = wcfxo_open; + wc->span.close = wcfxo_close; + wc->span.flags = ZT_FLAG_RBS; + init_waitqueue_head(&wc->span.maintq); + + wc->span.pvt = wc; + wc->chan.pvt = wc; + if (zt_register(&wc->span, 0)) { + printk("Unable to register span with zaptel\n"); + return -1; + } + return 0; +} + +static int wcfxo_hardware_init(struct wcfxo *wc) +{ + /* Hardware stuff */ + /* Reset PCI Interface chip and registers */ + outb(0x0e, wc->ioaddr + WC_CNTL); + if (wc->flags & FLAG_RESET_ON_AUX5) { + /* Set hook state to on hook for when we switch. + Make sure reset is high */ + outb(0x34, wc->ioaddr + WC_AUXD); + } else { + /* Set hook state to on hook for when we switch */ + outb(0x24, wc->ioaddr + WC_AUXD); + } + /* Set all to outputs except AUX 4, which is an input */ + outb(0xef, wc->ioaddr + WC_AUXC); + + /* Back to normal, with automatic DMA wrap around */ + outb(0x01, wc->ioaddr + WC_CNTL); + + /* Make sure serial port and DMA are out of reset */ + outb(inb(wc->ioaddr + WC_CNTL) & 0xf9, WC_CNTL); + + /* Configure serial port for MSB->LSB operation */ + if (wc->flags & FLAG_DOUBLE_CLOCK) + outb(0xc1, wc->ioaddr + WC_SERCTL); + else + outb(0xc0, wc->ioaddr + WC_SERCTL); + + if (wc->flags & FLAG_USE_XTAL) { + /* Use the crystal oscillator */ + outb(0x04, wc->ioaddr + WC_AUXFUNC); + } + + /* Delay FSC by 2 so it's properly aligned */ + outb(0x2, wc->ioaddr + WC_FSCDELAY); + + /* Setup DMA Addresses */ + outl(wc->writedma, wc->ioaddr + WC_DMAWS); /* Write start */ + outl(wc->writedma + ZT_CHUNKSIZE * 8, wc->ioaddr + WC_DMAWI); /* Middle (interrupt) */ + outl(wc->writedma + ZT_CHUNKSIZE * 16 - 4, wc->ioaddr + WC_DMAWE); /* End */ + + outl(wc->readdma, wc->ioaddr + WC_DMARS); /* Read start */ + outl(wc->readdma + ZT_CHUNKSIZE * 8, wc->ioaddr + WC_DMARI); /* Middle (interrupt) */ + outl(wc->readdma + ZT_CHUNKSIZE * 16 - 4, wc->ioaddr + WC_DMARE); /* End */ + + /* Clear interrupts */ + outb(0xff, wc->ioaddr + WC_INTSTAT); + return 0; +} + +static void wcfxo_enable_interrupts(struct wcfxo *wc) +{ + /* Enable interrupts (we care about all of them) */ + outb(0x3f, wc->ioaddr + WC_MASK0); + /* No external interrupts */ + outb(0x00, wc->ioaddr + WC_MASK1); +} + +static void wcfxo_start_dma(struct wcfxo *wc) +{ + /* Reset Master and TDM */ + outb(0x0f, wc->ioaddr + WC_CNTL); + set_current_state(TASK_INTERRUPTIBLE); + schedule_timeout(1); + outb(0x01, wc->ioaddr + WC_CNTL); + outb(0x01, wc->ioaddr + WC_OPER); +} + +static void wcfxo_stop_dma(struct wcfxo *wc) +{ + outb(0x00, wc->ioaddr + WC_OPER); +} + +static void wcfxo_disable_interrupts(struct wcfxo *wc) +{ + outb(0x00, wc->ioaddr + WC_MASK0); + outb(0x00, wc->ioaddr + WC_MASK1); +} + +static int wcfxo_init_daa(struct wcfxo *wc) +{ + /* This must not be called in an interrupt */ + /* We let things settle for a bit */ +// set_current_state(TASK_INTERRUPTIBLE); +// schedule_timeout(10); + + /* Soft-reset it */ + wcfxo_setreg(wc, 0x1, 0x80); + + /* Let the reset go */ + set_current_state(TASK_UNINTERRUPTIBLE); + schedule_timeout(1); + + /* We have a clock at 18.432 Mhz, so N1=1, M1=2, CGM=0 */ + wcfxo_setreg(wc, 0x7, 0x0); /* This value is N1 - 1 */ + wcfxo_setreg(wc, 0x8, 0x1); /* This value is M1 - 1 */ + /* We want to sample at 8khz, so N2 = 9, M2 = 10 (N2-1, M2-1) */ + wcfxo_setreg(wc, 0x9, 0x89); + + /* Wait until the PLL's are locked. Time is between 100 uSec and 1 mSec */ + set_current_state(TASK_INTERRUPTIBLE); + schedule_timeout(1); + + /* No additional ration is applied to the PLL and faster lock times + * are possible */ + wcfxo_setreg(wc, 0xa, 0x0); + /* Enable off hook pin */ + wcfxo_setreg(wc, 0x5, 0x0a); + /* Enable ISOcap and external speaker and charge pump if present */ + wcfxo_setreg(wc, 0x6, 0x80); + + + /* Wait a couple of jiffies for our writes to finish */ + set_current_state(TASK_INTERRUPTIBLE); + schedule_timeout(1); + + /* Didn't get it right. Register 9 is still garbage */ + if (wc->readregs[0x9] != 0x89) + return -1; +#if 0 + { int x; + int y; + for (y=0;y<100;y++) { + printk(" reg dump ====== %d ======\n", y); + for (x=0;xreadregs[wecareregs[x]]); + } + set_current_state(TASK_INTERRUPTIBLE); + schedule_timeout(100); + } } +#endif + return 0; +} + +static int __devinit wcfxo_init_one(struct pci_dev *pdev, const struct pci_device_id *ent) +{ + int res; + struct wcfxo *wc; + struct wcfxo_desc *d = (struct wcfxo_desc *)ent->driver_data; + int x; + static int initd_ifaces=0; + + if(initd_ifaces){ + memset((void *)ifaces,0,(sizeof(struct wcfxo *))*WC_MAX_IFACES); + initd_ifaces=1; + } + for (x=0;x= WC_MAX_IFACES) { + printk("Too many interfaces\n"); + return -EIO; + } + + if (pci_enable_device(pdev)) { + res = -EIO; + } else { + wc = kmalloc(sizeof(struct wcfxo), GFP_KERNEL); + if (wc) { + ifaces[x] = wc; + memset(wc, 0, sizeof(struct wcfxo)); + wc->ioaddr = pci_resource_start(pdev, 0); + wc->dev = pdev; + wc->pos = x; + wc->variety = d->name; + wc->flags = d->flags; + /* Keep track of whether we need to free the region */ + if (request_region(wc->ioaddr, 0xff, "wcfxo")) + wc->freeregion = 1; + + /* Allocate enough memory for two zt chunks, receive and transmit. Each sample uses + 32 bits. Allocate an extra set just for control too */ + wc->writechunk = (int *)pci_alloc_consistent(pdev, ZT_MAX_CHUNKSIZE * 2 * 2 * 2 * 4, &wc->writedma); + if (!wc->writechunk) { + printk("wcfxo: Unable to allocate DMA-able memory\n"); + if (wc->freeregion) + release_region(wc->ioaddr, 0xff); + return -ENOMEM; + } + + wc->readchunk = wc->writechunk + ZT_MAX_CHUNKSIZE * 4; /* in doublewords */ + wc->readdma = wc->writedma + ZT_MAX_CHUNKSIZE * 16; /* in bytes */ + + if (wcfxo_initialize(wc)) { + printk("wcfxo: Unable to intialize modem\n"); + if (wc->freeregion) + release_region(wc->ioaddr, 0xff); + kfree(wc); + return -EIO; + } + + /* Enable bus mastering */ + pci_set_master(pdev); + + /* Keep track of which device we are */ + pci_set_drvdata(pdev, wc); + + if (request_irq(pdev->irq, wcfxo_interrupt, SA_SHIRQ, "wcfxo", wc)) { + printk("wcfxo: Unable to request IRQ %d\n", pdev->irq); + if (wc->freeregion) + release_region(wc->ioaddr, 0xff); + kfree(wc); + return -EIO; + } + + + wcfxo_hardware_init(wc); + /* Enable interrupts */ + wcfxo_enable_interrupts(wc); + /* Initialize Write/Buffers to all blank data */ + memset((void *)wc->writechunk,0,ZT_MAX_CHUNKSIZE * 2 * 2 * 2 * 4); + /* Start DMA */ + wcfxo_start_dma(wc); + + /* Initialize DAA (after it's started) */ + if (wcfxo_init_daa(wc)) { + printk("Failed to initailize DAA, giving up...\n"); + wcfxo_stop_dma(wc); + wcfxo_disable_interrupts(wc); + zt_unregister(&wc->span); + free_irq(pdev->irq, wc); + + /* Reset PCI chip and registers */ + outb(0x0e, wc->ioaddr + WC_CNTL); + + if (wc->freeregion) + release_region(wc->ioaddr, 0xff); + kfree(wc); + return -EIO; + } + + printk("Found a Wildcard FXO: %s\n", wc->variety); + res = 0; + } else + res = -ENOMEM; + } + return res; +} + +static void wcfxo_release(struct wcfxo *wc) +{ + zt_unregister(&wc->span); + if (wc->freeregion) + release_region(wc->ioaddr, 0xff); + kfree(wc); + printk("Freed a Wildcard\n"); +} + +static void __devexit wcfxo_remove_one(struct pci_dev *pdev) +{ + struct wcfxo *wc = pci_get_drvdata(pdev); + if (wc) { + + /* Stop any DMA */ + wcfxo_stop_dma(wc); + + /* In case hardware is still there */ + wcfxo_disable_interrupts(wc); + + /* Immediately free resources */ + pci_free_consistent(pdev, ZT_MAX_CHUNKSIZE * 2 * 2 * 2 * 4, (void *)wc->writechunk, wc->writedma); + free_irq(pdev->irq, wc); + + /* Reset PCI chip and registers */ + outb(0x0e, wc->ioaddr + WC_CNTL); + + /* Release span, possibly delayed */ + if (!wc->usecount) + wcfxo_release(wc); + else + wc->dead = 1; + } +} + +static struct pci_device_id wcfxo_pci_tbl[] __devinitdata = { + { 0xe159, 0x0001, PCI_ANY_ID, PCI_ANY_ID, 0, 0, (unsigned long) &wcfxo }, + { 0x1057, 0x5608, PCI_ANY_ID, PCI_ANY_ID, 0, 0, (unsigned long) &wcx100p }, +}; + +static struct pci_driver wcfxo_driver = { + name: "wcfxo", + probe: wcfxo_init_one, + remove: wcfxo_remove_one, + suspend: NULL, + resume: NULL, + id_table: wcfxo_pci_tbl, +}; + +static int __init wcfxo_init(void) +{ + int res; + res = pci_module_init(&wcfxo_driver); + if (res) + return -ENODEV; + return 0; +} + +static void __exit wcfxo_cleanup(void) +{ + pci_unregister_driver(&wcfxo_driver); +} + +MODULE_PARM(debug, "i"); +MODULE_DESCRIPTION("Wildcard X100P Zaptel Driver"); +MODULE_AUTHOR("Mark Spencer "); + +module_init(wcfxo_init); +module_exit(wcfxo_cleanup); -- cgit v1.2.3