/* Cleopatre project {{{ * * Copyright (C) 2008 Spidcom * * <<>> * * }}} */ /** * \file src/plcdrv.c * \brief PLC driver management functions. * \ingroup plcdrv */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "plcdrv.h" #include "frame.h" #include "firmware.h" #include "boot_params.h" #include "registers.h" #define DRV_NAME "SPC300" #define DRV_LAYER "PLC " #define DRV_RELDATE __DATE__ " " __TIME__ MODULE_AUTHOR ("SPiDCOM Technologies"); MODULE_DESCRIPTION ("SPC300 PLC driver"); MODULE_LICENSE ("SPiDCOM Technologies 2009"); /** * Define DSU trace modes */ #define DSU_TRACE_NONE 0 #define DSU_TRACE_PROC 1 #define DSU_TRACE_AHB 2 #define DSU_TRACE_ALL 3 /** * Fake address used to detect that a leon_start_addr was provided as a * module param. */ #define INVALID_LEON_START_ADDR 1 /** Time Out for Leon start (in ms). */ #define TOUT_LEON_START 2000 /** Step Time Out for Leon start (in ms). */ #define TOUT_LEON_START_STEP 20 /** Parameters for the module. */ static uint32_t leon_start_addr = INVALID_LEON_START_ADDR; static uint32_t dsu_ctrl = (LEON_DSU_BZ | LEON_DSU_BD | LEON_DSU_BW | LEON_DSU_BE | LEON_DSU_TE); static uint32_t dsu_trace = DSU_TRACE_PROC; static int debug = 0; module_param (leon_start_addr, uint, 0644); MODULE_PARM_DESC (leon_start_addr, "PLC code start address"); module_param_string (boot_params, custom_boot_params, sizeof (custom_boot_params), 0644); MODULE_PARM_DESC (custom_boot_params, "PLC Boot Parameters"); module_param (dsu_ctrl, uint, 0644); MODULE_PARM_DESC (dsu_ctrl, "Configure the PLC Processor debugger"); module_param (dsu_trace, uint, 0644); MODULE_PARM_DESC (dsu_trace, "Configure the PLC Processor trace " "(0=no; 1=proc; 2=AHB; 3=all)"); module_param (debug, int, 0644); MODULE_PARM_DESC (debug, "Let's the user start PLC Processor by debugger"); /** These identify the driver base version. */ static char version[] __devinitdata = DRV_NAME " PLC driver v" DRV_VERSION " (" DRV_RELDATE ")\n"; /** Our global net device. */ static struct net_device *plcdrv_device; /** * Configure AFE with the default configuration. * \param dev net_device * \return negative value if error, 0 if no error */ static int plcdrv_config_afe (const struct net_device *dev) { struct afe *afe; int ret; afe = afe_get (); if (afe == NULL) { printk (KERN_ERR "%s: failed to get the AFE.\n", dev->name); return -EFAULT; } #ifdef CONFIG_MACH_ARIZONA ret = afe_write (afe, 0x03, 0x04) || afe_write (afe, 0x04, 0x35) || afe_write (afe, 0x06, 0x44) || afe_write (afe, 0x0A, 0x7F) || afe_write (afe, 0x0C, 0x43) || afe_write (afe, 0x0D, 0x01) || afe_write (afe, 0x0E, 0x80); #else ret = afe_write (afe, 0x04, 0x16) || afe_write (afe, 0x05, 0x80) || afe_write (afe, 0x07, 0x20) || afe_write (afe, 0x0A, 0x7F) //TODO: check diff values 9865 / 9867 || afe_write (afe, 0x0B, 0x20) || afe_write (afe, 0x0C, 0x51) //TODO: check diff values 9865 / 9867 || afe_write (afe, 0x0D, 0x01) || afe_write (afe, 0x0E, 0x08) || afe_write (afe, 0x03, 0xF8); #endif if (ret) printk (KERN_ERR "%s: error while configuring the AFE.\n", dev->name); return ret; } /** * Read Version number by /proc. * \param page buffer to write data to * \param start unused * \param offset unused * \param count unused * \param eof to be set if end of file reached * \param data priv structure * \return number of read bytes */ static int plcdrv_readproc_version (char *page, char **start, off_t offset, int count, int *eof, void *data) { plcdrv_t *priv = data; char *p; p = page; p += sprintf (p, "%s: %s\n", "PLC Driver", DRV_VERSION); p += sprintf (p, "%s: %s\n", "PLC Firmware", priv->version); *eof = 1; return p - page; } /** * Read plc stats. * \param page buffer to write data to * \param start unused * \param offset unused * \param count unused * \param eof to be set if end of file reached * \param data priv structure * \return number of read bytes */ static int plcdrv_readproc_plc_stats (char *page, char **start, off_t offset, int count, int *eof, void *data) { char *p; plcdrv_t *priv = data; plcdrv_stats_t *stats = &priv->plcdrv_stats; p = page; p += sprintf (p, "skb_to_fw_no_headroom: %d\n", atomic_read (&stats->skb_to_fw_no_headroom)); *eof = 1; return p - page; } /** * Read set mark. * \param page buffer to write data to * \param start unused * \param offset unused * \param count unused * \param eof to be set if end of file reached * \param data priv structure * \return number of read bytes */ static int plcdrv_readproc_set_mark (char *page, char **start, off_t offset, int count, int *eof, void *data) { plcdrv_t *priv = data; char *p; p = page; p += sprintf (p, "PLC_MARK %d\n", priv->qos.set_mark); *eof = 1; return p - page; } /** * Write set mark. * \param file unused * \param buffer user space buffer * \param count buffer length * \param data priv structure * \return error or number of written bytes */ static int plcdrv_writeproc_set_mark (struct file *file, const char *buffer, unsigned long count, void *data) { unsigned int set_mark; plcdrv_t *priv = data; const unsigned long size = 50; char kbuffer[size + 1]; /* Get buffer in kernel space. */ count = min (size, count); if (copy_from_user (kbuffer, buffer, count)) return -EFAULT; kbuffer[count] = '\0'; /* Parse and check. */ if (sscanf (kbuffer, "PLC_MARK %u", &set_mark) == 1) { if (set_mark < QOS_SET_MARK_NB) { priv->qos.set_mark = set_mark; return count; } } return -EINVAL; } /** * Set/Unset Reset the Leon processor. * \param activate true to activate reset * \return error code */ static int plcdrv_reset_leon (bool activate) { if (activate) { RB_RST_GROUP_VA |= RST_LEONSS; if (!(RB_RST_GROUP_VA & RST_LEONSS)) return -1; RB_RST_MODULE_VA |= RST_LCPU; if (!(RB_RST_MODULE_VA & RST_LCPU)) return -1; } else { RB_RST_GROUP_VA &= ~RST_LEONSS; if (RB_RST_GROUP_VA & RST_LEONSS) return -1; RB_RST_MODULE_VA &= ~RST_LCPU; if (RB_RST_MODULE_VA & RST_LCPU) return -1; } return 0; } /** * Set/Unset Reset the DSP. * \param activate true to activate reset * \return error code */ static int plcdrv_reset_dsp (bool activate) { if (activate) { RB_RST_GROUP_VA |= RST_DSP; if (!(RB_RST_GROUP_VA & RST_DSP)) return -1; } else { RB_RST_GROUP_VA &= ~RST_DSP; if (RB_RST_GROUP_VA & RST_DSP) return -1; } return 0; } /** * Launch the Leon processor. * \param priv PLC device private context * \param dsu_trace LEON trace mode * \param debug_mode 1 for debug mode, 0 otherwise * \return error code */ static int plcdrv_launch_leon (plcdrv_t *priv, uint32_t dsu_trace, int debug_mode) { uint32_t *leon_dsu_ptr; uint32_t *leon_wd_ptr; uint32_t *leon_trace_ptr; unsigned int timeout = 0; /* Prepare Leon registers accesses. */ leon_dsu_ptr = (uint32_t *) ioremap (LEON_DSU_CTRL_BASE_ADDR, 1); leon_trace_ptr = (uint32_t *) ioremap (LEON_TRACE_CTRL_BASE_ADDR, 1); leon_wd_ptr = (uint32_t *) ioremap (LEON_WD_BASE_ADDR, 1); /* Set trace mode for leon. */ switch (dsu_trace) { case DSU_TRACE_NONE: dsu_ctrl &= ~LEON_DSU_TE; *leon_trace_ptr &= ~(LEON_TRACE_PROC_EN | LEON_TRACE_AHB_EN); break; case DSU_TRACE_ALL: dsu_ctrl |= LEON_DSU_TE; *leon_trace_ptr |= (LEON_TRACE_PROC_EN | LEON_TRACE_AHB_EN); break; case DSU_TRACE_AHB: dsu_ctrl |= LEON_DSU_TE; *leon_trace_ptr &= ~(LEON_TRACE_PROC_EN); *leon_trace_ptr |= (LEON_TRACE_AHB_EN); break; case DSU_TRACE_PROC: default: dsu_ctrl |= LEON_DSU_TE; *leon_trace_ptr &= ~(LEON_TRACE_AHB_EN); *leon_trace_ptr |= (LEON_TRACE_PROC_EN); } /* Refresh watchdog before running (for 10 seconds). */ *leon_wd_ptr = LEON_WD_REFRESH (10); /* Configure DSU + Resume Leon processor execution. */ *leon_dsu_ptr = (dsu_ctrl & ~(LEON_DSU_BN | LEON_DSU_FT)); /* Check if processor is running. */ if ((*leon_dsu_ptr) & LEON_DSU_BN) return -1; /* Release reset Leon register. */ iounmap ((void *) leon_dsu_ptr); iounmap ((void *) leon_wd_ptr); iounmap ((void *) leon_trace_ptr); /* Now the Leon is started we have to wait leon mailbox initialization * before continuing. */ while (!ipmbox_is_synchronized (&priv->ipmbox)) { msleep_interruptible (TOUT_LEON_START_STEP); if (!debug_mode) { timeout += TOUT_LEON_START_STEP; if (timeout >= TOUT_LEON_START) return -1; } } return 0; } /** * Open the character device. * \param inp inode structure * \param filp file structure * \return error code */ static int plcdrv_char_open (struct inode *inp, struct file *filp) { plcdrv_t *priv = netdev_priv (plcdrv_device); /* Store private data. */ filp->private_data = priv; return 0; } /** * Poll the character device. * \param filp file structure * \param wait poll table structure * \return error code */ static unsigned int plcdrv_char_poll (struct file *filp, poll_table *wait) { plcdrv_t *priv = filp->private_data; poll_wait (filp, &priv->wd.wq, wait); if (atomic_read (&priv->wd.expired) == 1) return POLLPRI; return 0; } /** Character device functions */ static struct file_operations plcdrv_fops = { .owner = THIS_MODULE, .open = plcdrv_char_open, .poll = plcdrv_char_poll, }; /** * Read from trace character device. * \param filp file structure * \param buf user data pointer * \param f_pos position * \return error code */ static int plcdrv_trace_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { return debug_dump_read (filp->private_data, buf, count, f_pos); } /** * Open trace character device. * \param inp inode structure * \param filp file structure * \return error code */ static int plcdrv_trace_open (struct inode *inp, struct file *filp) { plcdrv_t *priv = netdev_priv (plcdrv_device); /* Store private data. */ filp->private_data = priv; /* Call open function. */ return debug_dump_open (priv); } /** * Release the trace character device. * \param inp inode structure * \param filp file structure * \return error code */ static int plcdrv_trace_release (struct inode *inp, struct file *filp) { debug_dump_release (filp->private_data); return 0; } /** File operations for trace char device. */ static struct file_operations plcdrv_fops_trace = { .owner = THIS_MODULE, .read = plcdrv_trace_read, .open = plcdrv_trace_open, .release = plcdrv_trace_release, }; /** * Transmit a frame received from DRV NETLINK. * \param skb frame structure */ static void plcdrv_netlink_tx (struct sk_buff *skb) { struct sk_buff *our_skb; plcdrv_t *priv = netdev_priv (plcdrv_device); /* Copy skb - will be freed by empty buffer later. */ our_skb = skb_copy (skb, GFP_ATOMIC); /* If allocation failed, nothing to do. */ if (!our_skb) return; /* Remove netlink header. */ skb_pull (our_skb, NLMSG_HDRLEN); /* Send it to MBX queue. */ frame_tx_mbx_mme_priv (priv, our_skb); } /** * Interrupt Handler Watchdog procedure. * \param irq interrupt number * \param dev_id device structure * \return error code */ static irqreturn_t plcdrv_it_wd (int irq, void *dev_id) { plcdrv_t *priv = netdev_priv (dev_id); /* Is some process waiting on a select()? */ if (!waitqueue_active (&priv->wd.wq)) { /* Reset. * But first, specify that the cause is actually the firmware watchdog. */ spc300_reset_cause_set_fw_wd (); /* Can't use one of the kernel reboot functions (like * kernel_restart()) because of the EXPORT_SYMBOL_GPL. */ arch_reset ('h'); } else { /* Wake up the process waiting on the select(), and let it handle the * situation. */ atomic_set (&priv->wd.expired, 1); wake_up (&priv->wd.wq); priv->ipmbox.regs->l2a_it_mask |= IPMBOX_L2A_IT_WD; } return IRQ_HANDLED; } /** * Interrupt Handler Receive procedure. * \param irq interrupt number * \param dev_id device structure * \return error code */ static irqreturn_t plcdrv_it_rx (int irq, void *dev_id) { struct net_device *dev = dev_id; plcdrv_t *priv = netdev_priv (dev); /* Mask interrupt. */ priv->ipmbox.regs->l2a_it_mask |= IPMBOX_L2A_IT; /* Schedule NAPI poll function. */ netif_rx_schedule (dev, &priv->napi); return IRQ_HANDLED; } /** * Read packet status from the device. * \param dev device structure * \return the device stats */ static struct net_device_stats * plcdrv_stats (struct net_device *dev) { plcdrv_t *priv = netdev_priv (dev); return &priv->stats; } /** * Change the MAC address. * \param dev device structure * \param p mac addr source * \return error code */ int plcdrv_set_mac_address (struct net_device *dev, void *p) { struct sockaddr *addr = p; memcpy (dev->dev_addr, addr->sa_data, dev->addr_len); return 0; } /** * User control device interface. * \param dev device structure * \param ifr user exchange structure * \param cmd command to execute * \return error code */ static int plcdrv_ioctl (struct net_device *dev, struct ifreq *ifr, int cmd) { int i; plcdrv_t *priv = netdev_priv (dev); struct plcdrv_setpid user_data; plcdrv_qos_rules_t qos_rules; qos_service_type_t qos_service_type; /* Find the command. */ switch (cmd) { case PLCDRV_IOCTL_SETQOS_RULES: /* Get user data */ if (copy_from_user (&qos_rules, ifr->ifr_data, sizeof (plcdrv_qos_rules_t))) return -EFAULT; /* Check user provided rules. */ if (qos_rules.rules_nb > PLCDRV_SERVICES_MAX_NB) return -EINVAL; for (i = 0; i < qos_rules.rules_nb; i++) { if (qos_rules.rules[i].packet_prio > (VLAN_PRIO_MASK >> VLAN_PRIO_SHIFT)) return -EINVAL; } /* Copy to context. */ priv->qos.rules = qos_rules; break; case PLCDRV_IOCTL_SETQOS_TYPE: /* Get user data */ if (copy_from_user (&qos_service_type, ifr->ifr_data, sizeof (qos_service_type_t))) return -EFAULT; /* Check user provided data. */ if (qos_service_type >= QOS_SERVICE_TYPE_NB) return -EINVAL; /* Copy to context. */ priv->qos.service_type = qos_service_type; break; case PLCDRV_IOCTL_SETPID: /* Get user data */ if (copy_from_user (&user_data, ifr->ifr_data, sizeof (user_data))) return -EFAULT; /* During initialization, plcd & managerd register their PID * for reception on DRV & MME netlink. */ if (NETLINK_PLC_DRV == user_data.nl) { priv->nl_plcd.pid = user_data.pid; printk (KERN_INFO "%s: plcd registered with PID %d\n", dev->name, priv->nl_plcd.pid); } if (NETLINK_PLC_MME == user_data.nl) { priv->nl_managerd.pid = user_data.pid; printk (KERN_INFO "%s: managerd registered with PID %d\n", dev->name, priv->nl_managerd.pid); } /* In case of another netlink, do nothing */ break; default: return -EOPNOTSUPP; } return 0; } /** * Initialize the device. * \param dev net device structure * \return error code */ static int plcdrv_open (struct net_device *dev) { plcdrv_t *priv = netdev_priv (dev); int ret; /* Request firmware. */ ret = firmware_load (dev); if (ret != 0) { printk (KERN_ERR "%s: error while loading firmware\n", dev->name); return ret; } /* Configure AFE with the default configuration. */ if (plcdrv_config_afe (dev)) return -EFAULT; /* Unreset Leon Processor. */ if (plcdrv_reset_leon (false)) return -1; /* Clear watchdog expiration flag. */ atomic_set (&priv->wd.expired, 0); /* Init IPMbox */ ret = ipmbox_init (&priv->ipmbox, dev, plcdrv_it_rx, plcdrv_it_wd); if (ret < 0) return ret; /* If we are here Leon code was already downloaded so, * we need to start the Leon processor. * It must be done after ARM head and tail pointers initialization because * firmware use it to set the rings base addresses. */ if (!debug) { ret = plcdrv_launch_leon (priv, dsu_trace, debug); if (ret < 0) goto clean_ipmbox; } /* Activate debug dump. */ debug_dump_start (priv); /* Allocate RX buffer pool to give to CESAR. */ skb_queue_head_init (&priv->rx_pool); if (!frame_buffer_alloc (priv)) { ret = -ENOMEM; goto clean_pool; } /* Initialize TX pool lists. */ skb_queue_head_init (&priv->tx_pool_data); skb_queue_head_init (&priv->tx_pool_mme); /* Initialize netlink functions. */ priv->nl_plcd.sock = netlink_kernel_create (&init_net, NETLINK_PLC_DRV, 0, plcdrv_netlink_tx, NULL, THIS_MODULE); if (!priv->nl_plcd.sock) { ret = -1; goto clean_pool; } priv->nl_managerd.sock = netlink_kernel_create (&init_net, NETLINK_PLC_MME, 0, plcdrv_netlink_tx, NULL, THIS_MODULE); if (!priv->nl_managerd.sock) { ret = -1; goto clean_nl_plcd; } /* Enable NAPI poll. */ napi_enable (&priv->napi); /* Prepare Linux as link up. */ netif_carrier_on (dev); netif_start_queue (dev); return 0; clean_nl_plcd: netlink_kernel_release (priv->nl_plcd.sock); clean_pool: __skb_queue_purge (&priv->rx_pool); clean_ipmbox: ipmbox_uninit (&priv->ipmbox, dev); return ret; } /** * Uninitialize the device. * \param dev device structure * \return error code */ static int plcdrv_stop (struct net_device *dev) { plcdrv_t *priv = netdev_priv (dev); /* Disable transmitter. */ netif_stop_queue (dev); netif_carrier_off (dev); /* Disable NAPI poll. */ napi_disable (&priv->napi); /* Stop netlink interface. */ netlink_kernel_release (priv->nl_plcd.sock); netlink_kernel_release (priv->nl_managerd.sock); /* Reset LEON and DSP. */ plcdrv_reset_leon (true); plcdrv_reset_dsp (true); plcdrv_reset_dsp (false); /* Deactivate debug dump. */ debug_dump_stop (priv); ipmbox_uninit (&priv->ipmbox, dev); /* Clean our queue, we can not rely on ipmbox content. */ __skb_queue_purge (&priv->rx_pool); __skb_queue_purge (&priv->tx_pool_data); __skb_queue_purge (&priv->tx_pool_mme); return 0; } /** * Uninitialise the network device. * \param dev device structure */ static void plcdrv_uninit (struct net_device *dev) { plcdrv_t *priv = netdev_priv (dev); /* Unregister character device. */ cdev_del (&priv->cdev); cdev_del (&priv->cdev_trace); unregister_chrdev_region (priv->dev_number, 2); /* Remove proc. */ remove_proc_entry ("set_mark", priv->proc_dir_plc); remove_proc_entry ("stats", priv->proc_dir_plc); remove_proc_entry ("version", priv->proc_dir_plc); remove_proc_entry ("plc", init_net.proc_net); } /** * Initialise the network device. * \param dev device structure * \return error code */ static int plcdrv_init (struct net_device *dev) { plcdrv_t *priv = netdev_priv (dev); uint32_t *leon_start_addr_ptr; int result; struct proc_dir_entry *entry; /* Initialise debug dump. */ debug_dump_init (priv); /* Register a character device to manage firmware binary download. */ /* Allocate character device Major number. */ result = alloc_chrdev_region (&priv->dev_number, 0, 2, "plcdrv"); if (result < 0) { printk (KERN_ERR DRV_NAME ": can't get major %d\n", MAJOR (priv->dev_number)); goto error_cdev_region_alloc; } /* Register PLC driver character device. */ cdev_init (&priv->cdev, &plcdrv_fops); priv->cdev.owner = THIS_MODULE; result = cdev_add (&priv->cdev, MKDEV (MAJOR (priv->dev_number), MINOR (priv->dev_number)), 1); if (result < 0) { printk (KERN_ERR DRV_NAME ": error registering plcdrv device\n"); goto error_cdev_add_plcdrv; } /* Register trace character device. */ cdev_init (&priv->cdev_trace, &plcdrv_fops_trace); priv->cdev_trace.owner = THIS_MODULE; result = cdev_add (&priv->cdev_trace, MKDEV (MAJOR (priv->dev_number), MINOR (priv->dev_number) + 1), 1); if (result < 0) { printk (KERN_ERR DRV_NAME ": error registering trace device\n"); goto error_cdev_add_trace; } /* Create proc entries. */ priv->proc_dir_plc = proc_mkdir ("plc", init_net.proc_net); create_proc_read_entry ("version", 0, priv->proc_dir_plc, plcdrv_readproc_version, priv); create_proc_read_entry ("stats", 0, priv->proc_dir_plc, plcdrv_readproc_plc_stats, priv); entry = create_proc_entry ("set_mark", 0, priv->proc_dir_plc); if (entry) { entry->read_proc = plcdrv_readproc_set_mark; entry->write_proc = plcdrv_writeproc_set_mark; entry->data = priv; } /* Set Leon code base address (translation). */ leon_start_addr_ptr = (uint32_t *) ioremap (RB_LEON_ADD_START, 1); /* If a custom value was passed as a module parameter, use it. * Otherwise, use the default one. */ *leon_start_addr_ptr = (leon_start_addr != INVALID_LEON_START_ADDR) ? leon_start_addr : spc300_plc_mem_start; iounmap (leon_start_addr_ptr); /* Initialise device functions. */ ether_setup (dev); dev->uninit = plcdrv_uninit; dev->open = plcdrv_open; dev->stop = plcdrv_stop; dev->do_ioctl = plcdrv_ioctl; dev->set_mac_address = plcdrv_set_mac_address; dev->hard_start_xmit = frame_tx_data; dev->get_stats = plcdrv_stats; /* Init NAPI structure. */ netif_napi_add (dev, &priv->napi, ipmbox_receive, IPMBOX_RX_BUDGET); /* Setup MAC address for Linux (stored in NVRAM). */ memcpy (dev->dev_addr, spc300_nvram.plc_address, ARRAY_SIZE (spc300_nvram.plc_address)); /* Initialize structure for poll/select management. */ init_waitqueue_head (&priv->wd.wq); seq_check_init (&priv->seq_check_ctx, "plc_drv"); return 0; error_cdev_add_trace: cdev_del (&priv->cdev); error_cdev_add_plcdrv: unregister_chrdev_region (priv->dev_number, 2); error_cdev_region_alloc: return result; } /** * Initialise the module. * \return error code */ int __init plcdrv_module_init (void) { int result; struct net_device *dev = NULL; struct plcdrv_t *priv; /* Print version. */ printk ("%s", version); /* Register the network device. */ /* Allocate device memory. */ dev = alloc_netdev (sizeof (plcdrv_t), "plc%d", ether_setup); if (!dev) return -ENOMEM; BUG_ON (!dev->priv); /* Initialize private structure. */ memset (dev->priv, 0, sizeof (plcdrv_t)); /* Proceed the init driver. */ dev->init = plcdrv_init; /* Store global plcdrv context. */ plcdrv_device = dev; priv = netdev_priv (dev); priv->dev = dev; /* Register network device. */ result = register_netdev (dev); if (result < 0) { /* Clean. */ free_netdev (dev); return result; } return 0; } /** * Uninitialise the module. */ void __exit plcdrv_module_exit (void) { /* Unregister network device. */ unregister_netdev (plcdrv_device); /* Free it. */ free_netdev (plcdrv_device); plcdrv_device = NULL; } module_init (plcdrv_module_init); module_exit (plcdrv_module_exit);