2005-04-17 06:20:36 +08:00
|
|
|
/*
|
|
|
|
* cycx_main.c Cyclades Cyclom 2X WAN Link Driver. Main module.
|
|
|
|
*
|
|
|
|
* Author: Arnaldo Carvalho de Melo <acme@conectiva.com.br>
|
|
|
|
*
|
|
|
|
* Copyright: (c) 1998-2003 Arnaldo Carvalho de Melo
|
|
|
|
*
|
|
|
|
* Based on sdlamain.c by Gene Kozin <genek@compuserve.com> &
|
|
|
|
* Jaspreet Singh <jaspreet@sangoma.com>
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
* ============================================================================
|
|
|
|
* Please look at the bitkeeper changelog (or any other scm tool that ends up
|
|
|
|
* importing bitkeeper changelog or that replaces bitkeeper in the future as
|
|
|
|
* main tool for linux development).
|
|
|
|
*
|
|
|
|
* 2001/05/09 acme Fix MODULE_DESC for debug, .bss nitpicks,
|
|
|
|
* some cleanups
|
|
|
|
* 2000/07/13 acme remove useless #ifdef MODULE and crap
|
|
|
|
* #if KERNEL_VERSION > blah
|
|
|
|
* 2000/07/06 acme __exit at cyclomx_cleanup
|
|
|
|
* 2000/04/02 acme dprintk and cycx_debug
|
|
|
|
* module_init/module_exit
|
|
|
|
* 2000/01/21 acme rename cyclomx_open to cyclomx_mod_inc_use_count
|
|
|
|
* and cyclomx_close to cyclomx_mod_dec_use_count
|
|
|
|
* 2000/01/08 acme cleanup
|
|
|
|
* 1999/11/06 acme cycx_down back to life (it needs to be
|
|
|
|
* called to iounmap the dpmbase)
|
|
|
|
* 1999/08/09 acme removed references to enable_tx_int
|
|
|
|
* use spinlocks instead of cli/sti in
|
|
|
|
* cyclomx_set_state
|
|
|
|
* 1999/05/19 acme works directly linked into the kernel
|
|
|
|
* init_waitqueue_head for 2.3.* kernel
|
|
|
|
* 1999/05/18 acme major cleanup (polling not needed), etc
|
|
|
|
* 1998/08/28 acme minor cleanup (ioctls for firmware deleted)
|
|
|
|
* queue_task activated
|
|
|
|
* 1998/08/08 acme Initial version.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <linux/stddef.h> /* offsetof(), etc. */
|
|
|
|
#include <linux/errno.h> /* return codes */
|
|
|
|
#include <linux/string.h> /* inline memset(), etc. */
|
|
|
|
#include <linux/slab.h> /* kmalloc(), kfree() */
|
|
|
|
#include <linux/kernel.h> /* printk(), and other useful stuff */
|
|
|
|
#include <linux/module.h> /* support for loadable modules */
|
|
|
|
#include <linux/ioport.h> /* request_region(), release_region() */
|
|
|
|
#include <linux/wanrouter.h> /* WAN router definitions */
|
|
|
|
#include <linux/cyclomx.h> /* cyclomx common user API definitions */
|
|
|
|
#include <linux/init.h> /* __init (when not using as a module) */
|
|
|
|
|
|
|
|
unsigned int cycx_debug;
|
|
|
|
|
|
|
|
MODULE_AUTHOR("Arnaldo Carvalho de Melo");
|
|
|
|
MODULE_DESCRIPTION("Cyclom 2X Sync Card Driver.");
|
|
|
|
MODULE_LICENSE("GPL");
|
|
|
|
module_param(cycx_debug, int, 0);
|
|
|
|
MODULE_PARM_DESC(cycx_debug, "cyclomx debug level");
|
|
|
|
|
|
|
|
/* Defines & Macros */
|
|
|
|
|
|
|
|
#define CYCX_DRV_VERSION 0 /* version number */
|
|
|
|
#define CYCX_DRV_RELEASE 11 /* release (minor version) number */
|
|
|
|
#define CYCX_MAX_CARDS 1 /* max number of adapters */
|
|
|
|
|
|
|
|
#define CONFIG_CYCX_CARDS 1
|
|
|
|
|
|
|
|
/* Function Prototypes */
|
|
|
|
|
|
|
|
/* WAN link driver entry points */
|
|
|
|
static int cycx_wan_setup(struct wan_device *wandev, wandev_conf_t *conf);
|
|
|
|
static int cycx_wan_shutdown(struct wan_device *wandev);
|
|
|
|
|
|
|
|
/* Miscellaneous functions */
|
IRQ: Maintain regs pointer globally rather than passing to IRQ handlers
Maintain a per-CPU global "struct pt_regs *" variable which can be used instead
of passing regs around manually through all ~1800 interrupt handlers in the
Linux kernel.
The regs pointer is used in few places, but it potentially costs both stack
space and code to pass it around. On the FRV arch, removing the regs parameter
from all the genirq function results in a 20% speed up of the IRQ exit path
(ie: from leaving timer_interrupt() to leaving do_IRQ()).
Where appropriate, an arch may override the generic storage facility and do
something different with the variable. On FRV, for instance, the address is
maintained in GR28 at all times inside the kernel as part of general exception
handling.
Having looked over the code, it appears that the parameter may be handed down
through up to twenty or so layers of functions. Consider a USB character
device attached to a USB hub, attached to a USB controller that posts its
interrupts through a cascaded auxiliary interrupt controller. A character
device driver may want to pass regs to the sysrq handler through the input
layer which adds another few layers of parameter passing.
I've build this code with allyesconfig for x86_64 and i386. I've runtested the
main part of the code on FRV and i386, though I can't test most of the drivers.
I've also done partial conversion for powerpc and MIPS - these at least compile
with minimal configurations.
This will affect all archs. Mostly the changes should be relatively easy.
Take do_IRQ(), store the regs pointer at the beginning, saving the old one:
struct pt_regs *old_regs = set_irq_regs(regs);
And put the old one back at the end:
set_irq_regs(old_regs);
Don't pass regs through to generic_handle_irq() or __do_IRQ().
In timer_interrupt(), this sort of change will be necessary:
- update_process_times(user_mode(regs));
- profile_tick(CPU_PROFILING, regs);
+ update_process_times(user_mode(get_irq_regs()));
+ profile_tick(CPU_PROFILING);
I'd like to move update_process_times()'s use of get_irq_regs() into itself,
except that i386, alone of the archs, uses something other than user_mode().
Some notes on the interrupt handling in the drivers:
(*) input_dev() is now gone entirely. The regs pointer is no longer stored in
the input_dev struct.
(*) finish_unlinks() in drivers/usb/host/ohci-q.c needs checking. It does
something different depending on whether it's been supplied with a regs
pointer or not.
(*) Various IRQ handler function pointers have been moved to type
irq_handler_t.
Signed-Off-By: David Howells <dhowells@redhat.com>
(cherry picked from 1b16e7ac850969f38b375e511e3fa2f474a33867 commit)
2006-10-05 21:55:46 +08:00
|
|
|
static irqreturn_t cycx_isr(int irq, void *dev_id);
|
2005-04-17 06:20:36 +08:00
|
|
|
|
|
|
|
/* Global Data
|
|
|
|
* Note: All data must be explicitly initialized!!!
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* private data */
|
|
|
|
static char cycx_drvname[] = "cyclomx";
|
|
|
|
static char cycx_fullname[] = "CYCLOM 2X(tm) Sync Card Driver";
|
|
|
|
static char cycx_copyright[] = "(c) 1998-2003 Arnaldo Carvalho de Melo "
|
|
|
|
"<acme@conectiva.com.br>";
|
|
|
|
static int cycx_ncards = CONFIG_CYCX_CARDS;
|
|
|
|
static struct cycx_device *cycx_card_array; /* adapter data space */
|
|
|
|
|
|
|
|
/* Kernel Loadable Module Entry Points */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Module 'insert' entry point.
|
|
|
|
* o print announcement
|
|
|
|
* o allocate adapter data space
|
|
|
|
* o initialize static data
|
|
|
|
* o register all cards with WAN router
|
|
|
|
* o calibrate Cyclom 2X shared memory access delay.
|
|
|
|
*
|
|
|
|
* Return: 0 Ok
|
|
|
|
* < 0 error.
|
|
|
|
* Context: process
|
|
|
|
*/
|
2005-09-10 14:17:28 +08:00
|
|
|
static int __init cycx_init(void)
|
2005-04-17 06:20:36 +08:00
|
|
|
{
|
|
|
|
int cnt, err = -ENOMEM;
|
|
|
|
|
|
|
|
printk(KERN_INFO "%s v%u.%u %s\n",
|
|
|
|
cycx_fullname, CYCX_DRV_VERSION, CYCX_DRV_RELEASE,
|
|
|
|
cycx_copyright);
|
|
|
|
|
|
|
|
/* Verify number of cards and allocate adapter data space */
|
|
|
|
cycx_ncards = min_t(int, cycx_ncards, CYCX_MAX_CARDS);
|
|
|
|
cycx_ncards = max_t(int, cycx_ncards, 1);
|
some kmalloc/memset ->kzalloc (tree wide)
Transform some calls to kmalloc/memset to a single kzalloc (or kcalloc).
Here is a short excerpt of the semantic patch performing
this transformation:
@@
type T2;
expression x;
identifier f,fld;
expression E;
expression E1,E2;
expression e1,e2,e3,y;
statement S;
@@
x =
- kmalloc
+ kzalloc
(E1,E2)
... when != \(x->fld=E;\|y=f(...,x,...);\|f(...,x,...);\|x=E;\|while(...) S\|for(e1;e2;e3) S\)
- memset((T2)x,0,E1);
@@
expression E1,E2,E3;
@@
- kzalloc(E1 * E2,E3)
+ kcalloc(E1,E2,E3)
[akpm@linux-foundation.org: get kcalloc args the right way around]
Signed-off-by: Yoann Padioleau <padator@wanadoo.fr>
Cc: Richard Henderson <rth@twiddle.net>
Cc: Ivan Kokshaysky <ink@jurassic.park.msu.ru>
Acked-by: Russell King <rmk@arm.linux.org.uk>
Cc: Bryan Wu <bryan.wu@analog.com>
Acked-by: Jiri Slaby <jirislaby@gmail.com>
Cc: Dave Airlie <airlied@linux.ie>
Acked-by: Roland Dreier <rolandd@cisco.com>
Cc: Jiri Kosina <jkosina@suse.cz>
Acked-by: Dmitry Torokhov <dtor@mail.ru>
Cc: Benjamin Herrenschmidt <benh@kernel.crashing.org>
Acked-by: Mauro Carvalho Chehab <mchehab@infradead.org>
Acked-by: Pierre Ossman <drzeus-list@drzeus.cx>
Cc: Jeff Garzik <jeff@garzik.org>
Cc: "David S. Miller" <davem@davemloft.net>
Acked-by: Greg KH <greg@kroah.com>
Cc: James Bottomley <James.Bottomley@steeleye.com>
Cc: "Antonino A. Daplas" <adaplas@pol.net>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2007-07-19 16:49:03 +08:00
|
|
|
cycx_card_array = kcalloc(cycx_ncards, sizeof(struct cycx_device), GFP_KERNEL);
|
2005-04-17 06:20:36 +08:00
|
|
|
if (!cycx_card_array)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
|
|
|
|
/* Register adapters with WAN router */
|
|
|
|
for (cnt = 0; cnt < cycx_ncards; ++cnt) {
|
|
|
|
struct cycx_device *card = &cycx_card_array[cnt];
|
|
|
|
struct wan_device *wandev = &card->wandev;
|
|
|
|
|
|
|
|
sprintf(card->devname, "%s%d", cycx_drvname, cnt + 1);
|
|
|
|
wandev->magic = ROUTER_MAGIC;
|
|
|
|
wandev->name = card->devname;
|
|
|
|
wandev->private = card;
|
|
|
|
wandev->setup = cycx_wan_setup;
|
|
|
|
wandev->shutdown = cycx_wan_shutdown;
|
|
|
|
err = register_wan_device(wandev);
|
|
|
|
|
|
|
|
if (err) {
|
|
|
|
printk(KERN_ERR "%s: %s registration failed with "
|
|
|
|
"error %d!\n",
|
|
|
|
cycx_drvname, card->devname, err);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = -ENODEV;
|
|
|
|
if (!cnt) {
|
|
|
|
kfree(cycx_card_array);
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
err = 0;
|
|
|
|
cycx_ncards = cnt; /* adjust actual number of cards */
|
|
|
|
out: return err;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Module 'remove' entry point.
|
|
|
|
* o unregister all adapters from the WAN router
|
|
|
|
* o release all remaining system resources
|
|
|
|
*/
|
|
|
|
static void __exit cycx_exit(void)
|
|
|
|
{
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
for (; i < cycx_ncards; ++i) {
|
|
|
|
struct cycx_device *card = &cycx_card_array[i];
|
|
|
|
unregister_wan_device(card->devname);
|
|
|
|
}
|
|
|
|
|
|
|
|
kfree(cycx_card_array);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* WAN Device Driver Entry Points */
|
|
|
|
/*
|
|
|
|
* Setup/configure WAN link driver.
|
|
|
|
* o check adapter state
|
|
|
|
* o make sure firmware is present in configuration
|
|
|
|
* o allocate interrupt vector
|
|
|
|
* o setup Cyclom 2X hardware
|
|
|
|
* o call appropriate routine to perform protocol-specific initialization
|
|
|
|
*
|
|
|
|
* This function is called when router handles ROUTER_SETUP IOCTL. The
|
|
|
|
* configuration structure is in kernel memory (including extended data, if
|
|
|
|
* any).
|
|
|
|
*/
|
|
|
|
static int cycx_wan_setup(struct wan_device *wandev, wandev_conf_t *conf)
|
|
|
|
{
|
|
|
|
int rc = -EFAULT;
|
|
|
|
struct cycx_device *card;
|
|
|
|
int irq;
|
|
|
|
|
|
|
|
/* Sanity checks */
|
|
|
|
|
|
|
|
if (!wandev || !wandev->private || !conf)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
card = wandev->private;
|
|
|
|
rc = -EBUSY;
|
|
|
|
if (wandev->state != WAN_UNCONFIGURED)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
rc = -EINVAL;
|
|
|
|
if (!conf->data_size || !conf->data) {
|
|
|
|
printk(KERN_ERR "%s: firmware not found in configuration "
|
|
|
|
"data!\n", wandev->name);
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (conf->irq <= 0) {
|
|
|
|
printk(KERN_ERR "%s: can't configure without IRQ!\n",
|
|
|
|
wandev->name);
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Allocate IRQ */
|
|
|
|
irq = conf->irq == 2 ? 9 : conf->irq; /* IRQ2 -> IRQ9 */
|
|
|
|
|
|
|
|
if (request_irq(irq, cycx_isr, 0, wandev->name, card)) {
|
|
|
|
printk(KERN_ERR "%s: can't reserve IRQ %d!\n",
|
|
|
|
wandev->name, irq);
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Configure hardware, load firmware, etc. */
|
|
|
|
memset(&card->hw, 0, sizeof(card->hw));
|
|
|
|
card->hw.irq = irq;
|
|
|
|
card->hw.dpmsize = CYCX_WINDOWSIZE;
|
|
|
|
card->hw.fwid = CFID_X25_2X;
|
|
|
|
spin_lock_init(&card->lock);
|
|
|
|
init_waitqueue_head(&card->wait_stats);
|
|
|
|
|
|
|
|
rc = cycx_setup(&card->hw, conf->data, conf->data_size, conf->maddr);
|
|
|
|
if (rc)
|
|
|
|
goto out_irq;
|
|
|
|
|
|
|
|
/* Initialize WAN device data space */
|
|
|
|
wandev->irq = irq;
|
|
|
|
wandev->dma = wandev->ioport = 0;
|
|
|
|
wandev->maddr = (unsigned long)card->hw.dpmbase;
|
|
|
|
wandev->msize = card->hw.dpmsize;
|
|
|
|
wandev->hw_opt[2] = 0;
|
|
|
|
wandev->hw_opt[3] = card->hw.fwid;
|
|
|
|
|
|
|
|
/* Protocol-specific initialization */
|
|
|
|
switch (card->hw.fwid) {
|
|
|
|
#ifdef CONFIG_CYCLOMX_X25
|
|
|
|
case CFID_X25_2X:
|
|
|
|
rc = cycx_x25_wan_init(card, conf);
|
|
|
|
break;
|
|
|
|
#endif
|
|
|
|
default:
|
|
|
|
printk(KERN_ERR "%s: this firmware is not supported!\n",
|
|
|
|
wandev->name);
|
|
|
|
rc = -EINVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rc) {
|
|
|
|
cycx_down(&card->hw);
|
|
|
|
goto out_irq;
|
|
|
|
}
|
|
|
|
|
|
|
|
rc = 0;
|
|
|
|
out:
|
|
|
|
return rc;
|
|
|
|
out_irq:
|
|
|
|
free_irq(irq, card);
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Shut down WAN link driver.
|
|
|
|
* o shut down adapter hardware
|
|
|
|
* o release system resources.
|
|
|
|
*
|
|
|
|
* This function is called by the router when device is being unregistered or
|
|
|
|
* when it handles ROUTER_DOWN IOCTL.
|
|
|
|
*/
|
|
|
|
static int cycx_wan_shutdown(struct wan_device *wandev)
|
|
|
|
{
|
|
|
|
int ret = -EFAULT;
|
|
|
|
struct cycx_device *card;
|
|
|
|
|
|
|
|
/* sanity checks */
|
|
|
|
if (!wandev || !wandev->private)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
ret = 0;
|
|
|
|
if (wandev->state == WAN_UNCONFIGURED)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
card = wandev->private;
|
|
|
|
wandev->state = WAN_UNCONFIGURED;
|
|
|
|
cycx_down(&card->hw);
|
|
|
|
printk(KERN_INFO "%s: irq %d being freed!\n", wandev->name,
|
|
|
|
wandev->irq);
|
|
|
|
free_irq(wandev->irq, card);
|
|
|
|
out: return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Miscellaneous */
|
|
|
|
/*
|
|
|
|
* Cyclom 2X Interrupt Service Routine.
|
|
|
|
* o acknowledge Cyclom 2X hardware interrupt.
|
|
|
|
* o call protocol-specific interrupt service routine, if any.
|
|
|
|
*/
|
IRQ: Maintain regs pointer globally rather than passing to IRQ handlers
Maintain a per-CPU global "struct pt_regs *" variable which can be used instead
of passing regs around manually through all ~1800 interrupt handlers in the
Linux kernel.
The regs pointer is used in few places, but it potentially costs both stack
space and code to pass it around. On the FRV arch, removing the regs parameter
from all the genirq function results in a 20% speed up of the IRQ exit path
(ie: from leaving timer_interrupt() to leaving do_IRQ()).
Where appropriate, an arch may override the generic storage facility and do
something different with the variable. On FRV, for instance, the address is
maintained in GR28 at all times inside the kernel as part of general exception
handling.
Having looked over the code, it appears that the parameter may be handed down
through up to twenty or so layers of functions. Consider a USB character
device attached to a USB hub, attached to a USB controller that posts its
interrupts through a cascaded auxiliary interrupt controller. A character
device driver may want to pass regs to the sysrq handler through the input
layer which adds another few layers of parameter passing.
I've build this code with allyesconfig for x86_64 and i386. I've runtested the
main part of the code on FRV and i386, though I can't test most of the drivers.
I've also done partial conversion for powerpc and MIPS - these at least compile
with minimal configurations.
This will affect all archs. Mostly the changes should be relatively easy.
Take do_IRQ(), store the regs pointer at the beginning, saving the old one:
struct pt_regs *old_regs = set_irq_regs(regs);
And put the old one back at the end:
set_irq_regs(old_regs);
Don't pass regs through to generic_handle_irq() or __do_IRQ().
In timer_interrupt(), this sort of change will be necessary:
- update_process_times(user_mode(regs));
- profile_tick(CPU_PROFILING, regs);
+ update_process_times(user_mode(get_irq_regs()));
+ profile_tick(CPU_PROFILING);
I'd like to move update_process_times()'s use of get_irq_regs() into itself,
except that i386, alone of the archs, uses something other than user_mode().
Some notes on the interrupt handling in the drivers:
(*) input_dev() is now gone entirely. The regs pointer is no longer stored in
the input_dev struct.
(*) finish_unlinks() in drivers/usb/host/ohci-q.c needs checking. It does
something different depending on whether it's been supplied with a regs
pointer or not.
(*) Various IRQ handler function pointers have been moved to type
irq_handler_t.
Signed-Off-By: David Howells <dhowells@redhat.com>
(cherry picked from 1b16e7ac850969f38b375e511e3fa2f474a33867 commit)
2006-10-05 21:55:46 +08:00
|
|
|
static irqreturn_t cycx_isr(int irq, void *dev_id)
|
2005-04-17 06:20:36 +08:00
|
|
|
{
|
2006-10-07 02:56:04 +08:00
|
|
|
struct cycx_device *card = dev_id;
|
2005-04-17 06:20:36 +08:00
|
|
|
|
2006-10-07 02:56:04 +08:00
|
|
|
if (card->wandev.state == WAN_UNCONFIGURED)
|
2005-04-17 06:20:36 +08:00
|
|
|
goto out;
|
|
|
|
|
|
|
|
if (card->in_isr) {
|
|
|
|
printk(KERN_WARNING "%s: interrupt re-entrancy on IRQ %d!\n",
|
|
|
|
card->devname, card->wandev.irq);
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (card->isr)
|
|
|
|
card->isr(card);
|
|
|
|
return IRQ_HANDLED;
|
|
|
|
out:
|
|
|
|
return IRQ_NONE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Set WAN device state. */
|
|
|
|
void cycx_set_state(struct cycx_device *card, int state)
|
|
|
|
{
|
|
|
|
unsigned long flags;
|
|
|
|
char *string_state = NULL;
|
|
|
|
|
|
|
|
spin_lock_irqsave(&card->lock, flags);
|
|
|
|
|
|
|
|
if (card->wandev.state != state) {
|
|
|
|
switch (state) {
|
|
|
|
case WAN_CONNECTED:
|
|
|
|
string_state = "connected!";
|
|
|
|
break;
|
|
|
|
case WAN_DISCONNECTED:
|
|
|
|
string_state = "disconnected!";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
printk(KERN_INFO "%s: link %s\n", card->devname, string_state);
|
|
|
|
card->wandev.state = state;
|
|
|
|
}
|
|
|
|
|
|
|
|
card->state_tick = jiffies;
|
|
|
|
spin_unlock_irqrestore(&card->lock, flags);
|
|
|
|
}
|
|
|
|
|
|
|
|
module_init(cycx_init);
|
|
|
|
module_exit(cycx_exit);
|