2007-05-08 11:32:15 +04:00
/*
* spidev . c - - simple synchronous userspace interface to SPI devices
*
* Copyright ( C ) 2006 SWAPP
* Andrea Paterniani < a . paterniani @ swapp - eng . it >
* Copyright ( C ) 2007 David Brownell ( simplification , cleanup )
*
* 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 0213 9 , USA .
*/
# include <linux/init.h>
# include <linux/module.h>
# include <linux/ioctl.h>
# include <linux/fs.h>
# include <linux/device.h>
2008-06-06 09:45:50 +04:00
# include <linux/err.h>
2007-05-08 11:32:15 +04:00
# include <linux/list.h>
# include <linux/errno.h>
# include <linux/mutex.h>
# include <linux/slab.h>
# include <linux/spi/spi.h>
# include <linux/spi/spidev.h>
# include <asm/uaccess.h>
/*
* This supports acccess to SPI devices using normal userspace I / O calls .
* Note that while traditional UNIX / POSIX I / O semantics are half duplex ,
* and often mask message boundaries , full SPI support requires full duplex
* transfers . There are several kinds of of internal message boundaries to
* handle chipselect management and other protocol options .
*
* SPI has a character major number assigned . We allocate minor numbers
* dynamically using a bitmask . You must use hotplug tools , such as udev
* ( or mdev with busybox ) to create and destroy the / dev / spidevB . C device
* nodes , since there is no fixed association of minor numbers with any
* particular SPI bus or device .
*/
# define SPIDEV_MAJOR 153 /* assigned */
# define N_SPI_MINORS 32 /* ... up to 256 */
static unsigned long minors [ N_SPI_MINORS / BITS_PER_LONG ] ;
2007-07-31 11:38:43 +04:00
/* Bit masks for spi_device.mode management. Note that incorrect
* settings for CS_HIGH and 3 WIRE can cause * lots * of trouble for other
* devices on a shared bus : CS_HIGH , because this device will be
* active when it shouldn ' t be ; 3 WIRE , because when active it won ' t
* behave as it should .
*
* REVISIT should changing those two modes be privileged ?
*/
# define SPI_MODE_MASK (SPI_CPHA | SPI_CPOL | SPI_CS_HIGH \
| SPI_LSB_FIRST | SPI_3WIRE | SPI_LOOP )
2007-05-08 11:32:15 +04:00
struct spidev_data {
2008-06-06 09:45:50 +04:00
dev_t devt ;
2008-05-24 00:05:03 +04:00
spinlock_t spi_lock ;
2007-05-08 11:32:15 +04:00
struct spi_device * spi ;
struct list_head device_entry ;
2008-06-06 09:45:50 +04:00
/* buffer is NULL unless this device is open (users > 0) */
2007-05-08 11:32:15 +04:00
struct mutex buf_lock ;
unsigned users ;
u8 * buffer ;
} ;
static LIST_HEAD ( device_list ) ;
static DEFINE_MUTEX ( device_list_lock ) ;
static unsigned bufsiz = 4096 ;
module_param ( bufsiz , uint , S_IRUGO ) ;
MODULE_PARM_DESC ( bufsiz , " data bytes in biggest supported SPI message " ) ;
/*-------------------------------------------------------------------------*/
2008-05-24 00:05:03 +04:00
/*
* We can ' t use the standard synchronous wrappers for file I / O ; we
* need to protect against async removal of the underlying spi_device .
*/
static void spidev_complete ( void * arg )
{
complete ( arg ) ;
}
static ssize_t
spidev_sync ( struct spidev_data * spidev , struct spi_message * message )
{
DECLARE_COMPLETION_ONSTACK ( done ) ;
int status ;
message - > complete = spidev_complete ;
message - > context = & done ;
spin_lock_irq ( & spidev - > spi_lock ) ;
if ( spidev - > spi = = NULL )
status = - ESHUTDOWN ;
else
status = spi_async ( spidev - > spi , message ) ;
spin_unlock_irq ( & spidev - > spi_lock ) ;
if ( status = = 0 ) {
wait_for_completion ( & done ) ;
status = message - > status ;
if ( status = = 0 )
status = message - > actual_length ;
}
return status ;
}
static inline ssize_t
spidev_sync_write ( struct spidev_data * spidev , size_t len )
{
struct spi_transfer t = {
. tx_buf = spidev - > buffer ,
. len = len ,
} ;
struct spi_message m ;
spi_message_init ( & m ) ;
spi_message_add_tail ( & t , & m ) ;
return spidev_sync ( spidev , & m ) ;
}
static inline ssize_t
spidev_sync_read ( struct spidev_data * spidev , size_t len )
{
struct spi_transfer t = {
. rx_buf = spidev - > buffer ,
. len = len ,
} ;
struct spi_message m ;
spi_message_init ( & m ) ;
spi_message_add_tail ( & t , & m ) ;
return spidev_sync ( spidev , & m ) ;
}
/*-------------------------------------------------------------------------*/
2007-05-08 11:32:15 +04:00
/* Read-only message with current device setup */
static ssize_t
spidev_read ( struct file * filp , char __user * buf , size_t count , loff_t * f_pos )
{
struct spidev_data * spidev ;
ssize_t status = 0 ;
/* chipselect only toggles at start or end of operation */
if ( count > bufsiz )
return - EMSGSIZE ;
spidev = filp - > private_data ;
mutex_lock ( & spidev - > buf_lock ) ;
2008-05-24 00:05:03 +04:00
status = spidev_sync_read ( spidev , count ) ;
2007-05-08 11:32:15 +04:00
if ( status = = 0 ) {
unsigned long missing ;
missing = copy_to_user ( buf , spidev - > buffer , count ) ;
if ( count & & missing = = count )
status = - EFAULT ;
else
status = count - missing ;
}
mutex_unlock ( & spidev - > buf_lock ) ;
return status ;
}
/* Write-only message with current device setup */
static ssize_t
spidev_write ( struct file * filp , const char __user * buf ,
size_t count , loff_t * f_pos )
{
struct spidev_data * spidev ;
ssize_t status = 0 ;
unsigned long missing ;
/* chipselect only toggles at start or end of operation */
if ( count > bufsiz )
return - EMSGSIZE ;
spidev = filp - > private_data ;
mutex_lock ( & spidev - > buf_lock ) ;
missing = copy_from_user ( spidev - > buffer , buf , count ) ;
if ( missing = = 0 ) {
2008-05-24 00:05:03 +04:00
status = spidev_sync_write ( spidev , count ) ;
2007-05-08 11:32:15 +04:00
if ( status = = 0 )
status = count ;
} else
status = - EFAULT ;
mutex_unlock ( & spidev - > buf_lock ) ;
return status ;
}
static int spidev_message ( struct spidev_data * spidev ,
struct spi_ioc_transfer * u_xfers , unsigned n_xfers )
{
struct spi_message msg ;
struct spi_transfer * k_xfers ;
struct spi_transfer * k_tmp ;
struct spi_ioc_transfer * u_tmp ;
unsigned n , total ;
u8 * buf ;
int status = - EFAULT ;
spi_message_init ( & msg ) ;
k_xfers = kcalloc ( n_xfers , sizeof ( * k_tmp ) , GFP_KERNEL ) ;
if ( k_xfers = = NULL )
return - ENOMEM ;
/* Construct spi_message, copying any tx data to bounce buffer.
* We walk the array of user - provided transfers , using each one
* to initialize a kernel version of the same transfer .
*/
mutex_lock ( & spidev - > buf_lock ) ;
buf = spidev - > buffer ;
total = 0 ;
for ( n = n_xfers , k_tmp = k_xfers , u_tmp = u_xfers ;
n ;
n - - , k_tmp + + , u_tmp + + ) {
k_tmp - > len = u_tmp - > len ;
2007-05-24 00:57:39 +04:00
total + = k_tmp - > len ;
if ( total > bufsiz ) {
status = - EMSGSIZE ;
goto done ;
}
2007-05-08 11:32:15 +04:00
if ( u_tmp - > rx_buf ) {
k_tmp - > rx_buf = buf ;
2007-08-11 00:01:09 +04:00
if ( ! access_ok ( VERIFY_WRITE , ( u8 __user * )
2007-10-29 08:11:28 +03:00
( uintptr_t ) u_tmp - > rx_buf ,
2007-08-11 00:01:09 +04:00
u_tmp - > len ) )
2007-05-08 11:32:15 +04:00
goto done ;
}
if ( u_tmp - > tx_buf ) {
k_tmp - > tx_buf = buf ;
2007-07-17 15:04:04 +04:00
if ( copy_from_user ( buf , ( const u8 __user * )
2007-10-29 08:11:28 +03:00
( uintptr_t ) u_tmp - > tx_buf ,
2007-05-08 11:32:15 +04:00
u_tmp - > len ) )
goto done ;
}
buf + = k_tmp - > len ;
k_tmp - > cs_change = ! ! u_tmp - > cs_change ;
k_tmp - > bits_per_word = u_tmp - > bits_per_word ;
k_tmp - > delay_usecs = u_tmp - > delay_usecs ;
k_tmp - > speed_hz = u_tmp - > speed_hz ;
# ifdef VERBOSE
dev_dbg ( & spi - > dev ,
" xfer len %zd %s%s%s%dbits %u usec %uHz \n " ,
u_tmp - > len ,
u_tmp - > rx_buf ? " rx " : " " ,
u_tmp - > tx_buf ? " tx " : " " ,
u_tmp - > cs_change ? " cs " : " " ,
u_tmp - > bits_per_word ? : spi - > bits_per_word ,
u_tmp - > delay_usecs ,
u_tmp - > speed_hz ? : spi - > max_speed_hz ) ;
# endif
spi_message_add_tail ( k_tmp , & msg ) ;
}
2008-05-24 00:05:03 +04:00
status = spidev_sync ( spidev , & msg ) ;
2007-05-08 11:32:15 +04:00
if ( status < 0 )
goto done ;
/* copy any rx data out of bounce buffer */
buf = spidev - > buffer ;
for ( n = n_xfers , u_tmp = u_xfers ; n ; n - - , u_tmp + + ) {
if ( u_tmp - > rx_buf ) {
2007-07-17 15:04:04 +04:00
if ( __copy_to_user ( ( u8 __user * )
2007-10-29 08:11:28 +03:00
( uintptr_t ) u_tmp - > rx_buf , buf ,
2007-05-08 11:32:15 +04:00
u_tmp - > len ) ) {
status = - EFAULT ;
goto done ;
}
}
buf + = u_tmp - > len ;
}
status = total ;
done :
mutex_unlock ( & spidev - > buf_lock ) ;
kfree ( k_xfers ) ;
return status ;
}
static int
spidev_ioctl ( struct inode * inode , struct file * filp ,
unsigned int cmd , unsigned long arg )
{
int err = 0 ;
int retval = 0 ;
struct spidev_data * spidev ;
struct spi_device * spi ;
u32 tmp ;
unsigned n_ioc ;
struct spi_ioc_transfer * ioc ;
/* Check type and command number */
if ( _IOC_TYPE ( cmd ) ! = SPI_IOC_MAGIC )
return - ENOTTY ;
/* Check access direction once here; don't repeat below.
* IOC_DIR is from the user perspective , while access_ok is
* from the kernel perspective ; so they look reversed .
*/
if ( _IOC_DIR ( cmd ) & _IOC_READ )
err = ! access_ok ( VERIFY_WRITE ,
( void __user * ) arg , _IOC_SIZE ( cmd ) ) ;
if ( err = = 0 & & _IOC_DIR ( cmd ) & _IOC_WRITE )
err = ! access_ok ( VERIFY_READ ,
( void __user * ) arg , _IOC_SIZE ( cmd ) ) ;
if ( err )
return - EFAULT ;
2008-05-24 00:05:03 +04:00
/* guard against device removal before, or while,
* we issue this ioctl .
*/
2007-05-08 11:32:15 +04:00
spidev = filp - > private_data ;
2008-05-24 00:05:03 +04:00
spin_lock_irq ( & spidev - > spi_lock ) ;
spi = spi_dev_get ( spidev - > spi ) ;
spin_unlock_irq ( & spidev - > spi_lock ) ;
if ( spi = = NULL )
return - ESHUTDOWN ;
2007-05-08 11:32:15 +04:00
switch ( cmd ) {
/* read requests */
case SPI_IOC_RD_MODE :
retval = __put_user ( spi - > mode & SPI_MODE_MASK ,
( __u8 __user * ) arg ) ;
break ;
case SPI_IOC_RD_LSB_FIRST :
retval = __put_user ( ( spi - > mode & SPI_LSB_FIRST ) ? 1 : 0 ,
( __u8 __user * ) arg ) ;
break ;
case SPI_IOC_RD_BITS_PER_WORD :
retval = __put_user ( spi - > bits_per_word , ( __u8 __user * ) arg ) ;
break ;
case SPI_IOC_RD_MAX_SPEED_HZ :
retval = __put_user ( spi - > max_speed_hz , ( __u32 __user * ) arg ) ;
break ;
/* write requests */
case SPI_IOC_WR_MODE :
retval = __get_user ( tmp , ( u8 __user * ) arg ) ;
if ( retval = = 0 ) {
u8 save = spi - > mode ;
if ( tmp & ~ SPI_MODE_MASK ) {
retval = - EINVAL ;
break ;
}
tmp | = spi - > mode & ~ SPI_MODE_MASK ;
spi - > mode = ( u8 ) tmp ;
retval = spi_setup ( spi ) ;
if ( retval < 0 )
spi - > mode = save ;
else
dev_dbg ( & spi - > dev , " spi mode %02x \n " , tmp ) ;
}
break ;
case SPI_IOC_WR_LSB_FIRST :
retval = __get_user ( tmp , ( __u8 __user * ) arg ) ;
if ( retval = = 0 ) {
u8 save = spi - > mode ;
if ( tmp )
spi - > mode | = SPI_LSB_FIRST ;
else
spi - > mode & = ~ SPI_LSB_FIRST ;
retval = spi_setup ( spi ) ;
if ( retval < 0 )
spi - > mode = save ;
else
dev_dbg ( & spi - > dev , " %csb first \n " ,
tmp ? ' l ' : ' m ' ) ;
}
break ;
case SPI_IOC_WR_BITS_PER_WORD :
retval = __get_user ( tmp , ( __u8 __user * ) arg ) ;
if ( retval = = 0 ) {
u8 save = spi - > bits_per_word ;
spi - > bits_per_word = tmp ;
retval = spi_setup ( spi ) ;
if ( retval < 0 )
spi - > bits_per_word = save ;
else
dev_dbg ( & spi - > dev , " %d bits per word \n " , tmp ) ;
}
break ;
case SPI_IOC_WR_MAX_SPEED_HZ :
retval = __get_user ( tmp , ( __u32 __user * ) arg ) ;
if ( retval = = 0 ) {
u32 save = spi - > max_speed_hz ;
spi - > max_speed_hz = tmp ;
retval = spi_setup ( spi ) ;
if ( retval < 0 )
spi - > max_speed_hz = save ;
else
dev_dbg ( & spi - > dev , " %d Hz (max) \n " , tmp ) ;
}
break ;
default :
/* segmented and/or full-duplex I/O request */
if ( _IOC_NR ( cmd ) ! = _IOC_NR ( SPI_IOC_MESSAGE ( 0 ) )
2008-05-24 00:05:03 +04:00
| | _IOC_DIR ( cmd ) ! = _IOC_WRITE ) {
retval = - ENOTTY ;
break ;
}
2007-05-08 11:32:15 +04:00
tmp = _IOC_SIZE ( cmd ) ;
if ( ( tmp % sizeof ( struct spi_ioc_transfer ) ) ! = 0 ) {
retval = - EINVAL ;
break ;
}
n_ioc = tmp / sizeof ( struct spi_ioc_transfer ) ;
if ( n_ioc = = 0 )
break ;
/* copy into scratch area */
ioc = kmalloc ( tmp , GFP_KERNEL ) ;
if ( ! ioc ) {
retval = - ENOMEM ;
break ;
}
if ( __copy_from_user ( ioc , ( void __user * ) arg , tmp ) ) {
2007-05-24 00:57:45 +04:00
kfree ( ioc ) ;
2007-05-08 11:32:15 +04:00
retval = - EFAULT ;
break ;
}
/* translate to spi_message, execute */
retval = spidev_message ( spidev , ioc , n_ioc ) ;
kfree ( ioc ) ;
break ;
}
2008-05-24 00:05:03 +04:00
spi_dev_put ( spi ) ;
2007-05-08 11:32:15 +04:00
return retval ;
}
static int spidev_open ( struct inode * inode , struct file * filp )
{
struct spidev_data * spidev ;
int status = - ENXIO ;
mutex_lock ( & device_list_lock ) ;
list_for_each_entry ( spidev , & device_list , device_entry ) {
2008-06-06 09:45:50 +04:00
if ( spidev - > devt = = inode - > i_rdev ) {
2007-05-08 11:32:15 +04:00
status = 0 ;
break ;
}
}
if ( status = = 0 ) {
if ( ! spidev - > buffer ) {
spidev - > buffer = kmalloc ( bufsiz , GFP_KERNEL ) ;
if ( ! spidev - > buffer ) {
dev_dbg ( & spidev - > spi - > dev , " open/ENOMEM \n " ) ;
status = - ENOMEM ;
}
}
if ( status = = 0 ) {
spidev - > users + + ;
filp - > private_data = spidev ;
nonseekable_open ( inode , filp ) ;
}
} else
pr_debug ( " spidev: nothing for minor %d \n " , iminor ( inode ) ) ;
mutex_unlock ( & device_list_lock ) ;
return status ;
}
static int spidev_release ( struct inode * inode , struct file * filp )
{
struct spidev_data * spidev ;
int status = 0 ;
mutex_lock ( & device_list_lock ) ;
spidev = filp - > private_data ;
filp - > private_data = NULL ;
2008-06-06 09:45:50 +04:00
/* last close? */
2007-05-08 11:32:15 +04:00
spidev - > users - - ;
if ( ! spidev - > users ) {
2008-06-06 09:45:50 +04:00
int dofree ;
2007-05-08 11:32:15 +04:00
kfree ( spidev - > buffer ) ;
spidev - > buffer = NULL ;
2008-06-06 09:45:50 +04:00
/* ... after we unbound from the underlying device? */
spin_lock_irq ( & spidev - > spi_lock ) ;
dofree = ( spidev - > spi = = NULL ) ;
spin_unlock_irq ( & spidev - > spi_lock ) ;
if ( dofree )
kfree ( spidev ) ;
2007-05-08 11:32:15 +04:00
}
mutex_unlock ( & device_list_lock ) ;
return status ;
}
static struct file_operations spidev_fops = {
. owner = THIS_MODULE ,
/* REVISIT switch to aio primitives, so that userspace
* gets more complete API coverage . It ' ll simplify things
* too , except for the locking .
*/
. write = spidev_write ,
. read = spidev_read ,
. ioctl = spidev_ioctl ,
. open = spidev_open ,
. release = spidev_release ,
} ;
/*-------------------------------------------------------------------------*/
/* The main reason to have this class is to make mdev/udev create the
* / dev / spidevB . C character device nodes exposing our userspace API .
* It also simplifies memory management .
*/
2008-06-06 09:45:50 +04:00
static struct class * spidev_class ;
2007-05-08 11:32:15 +04:00
/*-------------------------------------------------------------------------*/
static int spidev_probe ( struct spi_device * spi )
{
struct spidev_data * spidev ;
int status ;
unsigned long minor ;
/* Allocate driver data */
spidev = kzalloc ( sizeof ( * spidev ) , GFP_KERNEL ) ;
if ( ! spidev )
return - ENOMEM ;
/* Initialize the driver data */
spidev - > spi = spi ;
2008-05-24 00:05:03 +04:00
spin_lock_init ( & spidev - > spi_lock ) ;
2007-05-08 11:32:15 +04:00
mutex_init ( & spidev - > buf_lock ) ;
INIT_LIST_HEAD ( & spidev - > device_entry ) ;
/* If we can allocate a minor number, hook up this device.
* Reusing minors is fine so long as udev or mdev is working .
*/
mutex_lock ( & device_list_lock ) ;
2007-05-16 10:57:05 +04:00
minor = find_first_zero_bit ( minors , N_SPI_MINORS ) ;
2007-05-08 11:32:15 +04:00
if ( minor < N_SPI_MINORS ) {
2008-06-06 09:45:50 +04:00
struct device * dev ;
spidev - > devt = MKDEV ( SPIDEV_MAJOR , minor ) ;
dev = device_create ( spidev_class , & spi - > dev , spidev - > devt ,
2007-05-08 11:32:15 +04:00
" spidev%d.%d " ,
spi - > master - > bus_num , spi - > chip_select ) ;
2008-06-06 09:45:50 +04:00
status = IS_ERR ( dev ) ? PTR_ERR ( dev ) : 0 ;
2007-05-08 11:32:15 +04:00
} else {
dev_dbg ( & spi - > dev , " no minor number available! \n " ) ;
status = - ENODEV ;
}
if ( status = = 0 ) {
set_bit ( minor , minors ) ;
2008-06-06 09:45:50 +04:00
spi_set_drvdata ( spi , spidev ) ;
2007-05-08 11:32:15 +04:00
list_add ( & spidev - > device_entry , & device_list ) ;
}
mutex_unlock ( & device_list_lock ) ;
if ( status ! = 0 )
kfree ( spidev ) ;
return status ;
}
static int spidev_remove ( struct spi_device * spi )
{
2008-06-06 09:45:50 +04:00
struct spidev_data * spidev = spi_get_drvdata ( spi ) ;
2007-05-08 11:32:15 +04:00
2008-05-24 00:05:03 +04:00
/* make sure ops on existing fds can abort cleanly */
spin_lock_irq ( & spidev - > spi_lock ) ;
spidev - > spi = NULL ;
2008-06-06 09:45:50 +04:00
spi_set_drvdata ( spi , NULL ) ;
2008-05-24 00:05:03 +04:00
spin_unlock_irq ( & spidev - > spi_lock ) ;
2007-05-08 11:32:15 +04:00
2008-05-24 00:05:03 +04:00
/* prevent new opens */
mutex_lock ( & device_list_lock ) ;
2007-05-08 11:32:15 +04:00
list_del ( & spidev - > device_entry ) ;
2008-06-06 09:45:50 +04:00
device_destroy ( spidev_class , spidev - > devt ) ;
clear_bit ( MINOR ( spidev - > devt ) , minors ) ;
if ( spidev - > users = = 0 )
kfree ( spidev ) ;
2007-05-08 11:32:15 +04:00
mutex_unlock ( & device_list_lock ) ;
return 0 ;
}
static struct spi_driver spidev_spi = {
. driver = {
. name = " spidev " ,
. owner = THIS_MODULE ,
} ,
. probe = spidev_probe ,
. remove = __devexit_p ( spidev_remove ) ,
/* NOTE: suspend/resume methods are not necessary here.
* We don ' t do anything except pass the requests to / from
* the underlying controller . The refrigerator handles
* most issues ; the controller driver handles the rest .
*/
} ;
/*-------------------------------------------------------------------------*/
static int __init spidev_init ( void )
{
int status ;
/* Claim our 256 reserved device numbers. Then register a class
* that will key udev / mdev to add / remove / dev nodes . Last , register
* the driver which manages those device numbers .
*/
BUILD_BUG_ON ( N_SPI_MINORS > 256 ) ;
status = register_chrdev ( SPIDEV_MAJOR , " spi " , & spidev_fops ) ;
if ( status < 0 )
return status ;
2008-06-06 09:45:50 +04:00
spidev_class = class_create ( THIS_MODULE , " spidev " ) ;
if ( IS_ERR ( spidev_class ) ) {
2007-05-08 11:32:15 +04:00
unregister_chrdev ( SPIDEV_MAJOR , spidev_spi . driver . name ) ;
2008-06-06 09:45:50 +04:00
return PTR_ERR ( spidev_class ) ;
2007-05-08 11:32:15 +04:00
}
status = spi_register_driver ( & spidev_spi ) ;
if ( status < 0 ) {
2008-06-06 09:45:50 +04:00
class_destroy ( spidev_class ) ;
2007-05-08 11:32:15 +04:00
unregister_chrdev ( SPIDEV_MAJOR , spidev_spi . driver . name ) ;
}
return status ;
}
module_init ( spidev_init ) ;
static void __exit spidev_exit ( void )
{
spi_unregister_driver ( & spidev_spi ) ;
2008-06-06 09:45:50 +04:00
class_destroy ( spidev_class ) ;
2007-05-08 11:32:15 +04:00
unregister_chrdev ( SPIDEV_MAJOR , spidev_spi . driver . name ) ;
}
module_exit ( spidev_exit ) ;
MODULE_AUTHOR ( " Andrea Paterniani, <a.paterniani@swapp-eng.it> " ) ;
MODULE_DESCRIPTION ( " User mode SPI device interface " ) ;
MODULE_LICENSE ( " GPL " ) ;