一、spidev简单介绍
如果在内核中配置spidev,会在“/dev”目录下产生设备节点,通过此节点可以操作挂载在该SPI总线上的设备,接下来将从驱动层和应用层
来分析程序。
二、spidev驱动层
2.1、驱动注册
分析一个设备驱动,一般都是从module_init和module_exit处开始,本文也不例外,程序如下:
-
#define SPIDEV_MAJOR 153 /* assigned */
-
#define N_SPI_MINORS 32 /* ... up to 256 */
-
-
static DECLARE_BITMAP(minors, N_SPI_MINORS);
-
static struct spi_driver spidev_spi_driver = {
-
.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;
-
-
spidev_class = class_create(THIS_MODULE, "spidev");
-
if (IS_ERR(spidev_class)) {
-
unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
-
return PTR_ERR(spidev_class);
-
}
-
-
status = spi_register_driver(&spidev_spi_driver);
-
if (status < 0) {
-
class_destroy(spidev_class);
-
unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
-
}
-
return status;
-
}
-
module_init(spidev_init);
-
-
static void __exit spidev_exit(void)
-
{
-
spi_unregister_driver(&spidev_spi_driver);
-
class_destroy(spidev_class);
-
unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.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");
-
MODULE_ALIAS("spi:spidev");
说明:
1) 在驱动注册函数中,首先注册函数操作集spidev_fops,该内容将在2.3中具体讲述。
2) 生成spidev设备类,此类的表现是在“/sys/class”目录下生成一个名为spidev目录。
3) 注册spi驱动。
4) 退出函数是注册函数的相反过程,就是释放注册函数中注册的资源。
2.2、探测和移除函数
探测函数程序如下:
-
static int __devinit 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;
-
spin_lock_init(&spidev->spi_lock);
-
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);
-
minor = find_first_zero_bit(minors, N_SPI_MINORS);
-
if (minor < N_SPI_MINORS) {
-
struct device *dev;
-
-
spidev->devt = MKDEV(SPIDEV_MAJOR, minor);
-
dev = device_create(spidev_class, &spi->dev, spidev->devt,
-
spidev, "spidev%d.%d",
-
spi->master->bus_num, spi->chip_select);
-
status = IS_ERR(dev) ? PTR_ERR(dev) : 0;
-
} else {
-
dev_dbg(&spi->dev, "no minor number available!\n");
-
status = -ENODEV;
-
}
-
if (status == 0) {
-
set_bit(minor, minors);
-
list_add(&spidev->device_entry, &device_list);
-
}
-
mutex_unlock(&device_list_lock);
-
-
if (status == 0)
-
spi_set_drvdata(spi, spidev);
-
else
-
kfree(spidev);
-
-
return status;
-
}
说明:
1) 首先声明spidev_data结构体内存。
2) 初始化其成员变量。
3) 在位图中寻找第一个未被用到的位。
4) 如果位图寻找正确,创建“/dev”目录下的设备节点。
4) 将spidev结构体中的链表插入局部全局链表device_list中,以便在函数操作集中的open函数中使用
移除函数如下:
-
static int __devexit spidev_remove(struct spi_device *spi)
-
{
-
struct spidev_data *spidev = spi_get_drvdata(spi);
-
-
/* make sure ops on existing fds can abort cleanly */
-
spin_lock_irq(&spidev->spi_lock);
-
spidev->spi = NULL;
-
spi_set_drvdata(spi, NULL);
-
spin_unlock_irq(&spidev->spi_lock);
-
-
/* prevent new opens */
-
mutex_lock(&device_list_lock);
-
list_del(&spidev->device_entry);
-
device_destroy(spidev_class, spidev->devt);
-
clear_bit(MINOR(spidev->devt), minors);
-
if (spidev->users == 0)
-
kfree(spidev);
-
mutex_unlock(&device_list_lock);
-
-
return 0;
-
}
说明:
1) 移除函数就是探测函数的相反过程,主要就是释放探测函数中申请的资源。
2.2、函数操作集spidev_fop
spidev_fop具体如下:
-
static const 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,
-
.unlocked_ioctl = spidev_ioctl,
-
.compat_ioctl = spidev_compat_ioctl,
-
.open = spidev_open,
-
.release = spidev_release,
-
.llseek = no_llseek,
-
};
首先看下打开函数spidev_open,程序如下:
-
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) {
-
if (spidev->devt == inode->i_rdev) {
-
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;
-
}
说明:
1) 程序首先上本地全局互斥锁。
2) 遍历链表device_list,通过设备号找到在探测函数中创建的设备结构体spidev。
3) 如果寻找成功,将其保存在file的私有成员中,供其他操作集中的函数使用。
realease函数如下:
-
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;
-
-
/* last close? */
-
spidev->users--;
-
if (!spidev->users) {
-
int dofree;
-
-
kfree(spidev->buffer);
-
spidev->buffer = NULL;
-
-
/* ... 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);
-
}
-
mutex_unlock(&device_list_lock);
-
-
return status;
-
}
说明:
1) 首先减少使用次数。
2) 释放传输buf空间。
3) 释放结构体内存。
spi读数据函数如下:
-
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);
-
}
-
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);
-
status = spidev_sync_read(spidev, count);
-
if (status > 0) {
-
unsigned long missing;
-
-
missing = copy_to_user(buf, spidev->buffer, status);
-
if (missing == status)
-
status = -EFAULT;
-
else
-
status = status - missing;
-
}
-
mutex_unlock(&spidev->buf_lock);
-
-
return status;
-
}
说明:
1) 进入读函数中,首先上互斥锁。
2) 然后调用spidev_sync_read函数读数据。
3) 如果读取成功,将读取到的数据拷贝到应用层。
4) 解互斥锁。
5) 在spidev_sync_read函数中,首先定义一个传输结构体spi_transfer,由于是读操作,所以只初始化其接收buf。
6) 初始化spi_message,调用函数spidev_sync传输数据,其具体程序如下:
-
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;
-
}
说明:
1) 首先定义并初始化一个completion。
2) 上自旋锁,调用spi_async函数传输数据,spi_async就是在Linux核心中提供的传输函数。
3) 如果调用传输函数成功,则等待。
3) wait_for_completion(&done);等待传输完成,如果传输正确,函数返回值为传输的字节数。
函数操作集写函数spidev_write程序如下:
-
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 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) {
-
status = spidev_sync_write(spidev, count);
-
} else
-
status = -EFAULT;
-
mutex_unlock(&spidev->buf_lock);
-
-
return status;
-
}
说明:
1) 写函数首先上互斥锁,然后从应用层拷贝需要写的数据。
2) 如果拷贝成功,调用函数spidev_sync_write完成写数据。
3) 解互斥锁。
4) spidev_sync_write函数中,首先定义spi_transfer传输结构体,由于是写,此结构体只有tx_buf。
5) 初始化spi_message,然后调用spidev_sync传输数据,此函数在上面的读操作中已经讲述。
ioctl函数如下:
-
static long
-
spidev_ioctl(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;
-
-
/* guard against device removal before, or while,
-
* we issue this ioctl.
-
*/
-
spidev = filp->private_data;
-
spin_lock_irq(&spidev->spi_lock);
-
spi = spi_dev_get(spidev->spi);
-
spin_unlock_irq(&spidev->spi_lock);
-
-
if (spi == NULL)
-
return -ESHUTDOWN;
-
-
/* use the buffer lock here for triple duty:
-
* - prevent I/O (from us) so calling spi_setup() is safe;
-
* - prevent concurrent SPI_IOC_WR_* from morphing
-
* data fields while SPI_IOC_RD_* reads them;
-
* - SPI_IOC_MESSAGE needs the buffer locked "normally".
-
*/
-
mutex_lock(&spidev->buf_lock);
-
-
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))
-
|| _IOC_DIR(cmd) != _IOC_WRITE) {
-
retval = -ENOTTY;
-
break;
-
}
-
-
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)) {
-
kfree(ioc);
-
retval = -EFAULT;
-
break;
-
}
-
-
/* translate to spi_message, execute */
-
retval = spidev_message(spidev, ioc, n_ioc);
-
kfree(ioc);
-
break;
-
}
-
-
mutex_unlock(&spidev->buf_lock);
-
spi_dev_put(spi);
-
return retval;
-
}
说明:
1) 函数首先判断命令是否有效,如果无效,直接退出。
2) 上互斥锁。
3) switch分支中,前面的case都是支持对SPI设备的设置,包括模式、传输位、位方向和最大速率等。
4) 在设置分支中,最后调用spi_setup实现设置,此函数最终是调用总线驱动中的gsc3280_spi_setup(struct spi_device *spi)
实现设置。
5) default分支中是进行数据传输的分支,首先判断是否是有效的数据,如果不是,退出switch分支。
6) 申请内存,复制从应用层传过来的数据。
7) 调用spidev_message函数实现数据传输。
8) 解互斥锁,退出。
spidev_message函数如下:
-
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.
-
*/
-
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;
-
-
total += k_tmp->len;
-
if (total > bufsiz) {
-
status = -EMSGSIZE;
-
goto done;
-
}
-
-
if (u_tmp->rx_buf) {
-
k_tmp->rx_buf = buf;
-
if (!access_ok(VERIFY_WRITE, (u8 __user *)
-
(uintptr_t) u_tmp->rx_buf,
-
u_tmp->len))
-
goto done;
-
}
-
if (u_tmp->tx_buf) {
-
k_tmp->tx_buf = buf;
-
if (copy_from_user(buf, (const u8 __user *)
-
(uintptr_t) u_tmp->tx_buf,
-
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(&spidev->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 ? : spidev->spi->bits_per_word,
-
u_tmp->delay_usecs,
-
u_tmp->speed_hz ? : spidev->spi->max_speed_hz);
-
#endif
-
spi_message_add_tail(k_tmp, &msg);
-
}
-
-
status = spidev_sync(spidev, &msg);
-
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) {
-
if (__copy_to_user((u8 __user *)
-
(uintptr_t) u_tmp->rx_buf, buf,
-
u_tmp->len)) {
-
status = -EFAULT;
-
goto done;
-
}
-
}
-
buf += u_tmp->len;
-
}
-
status = total;
-
-
done:
-
kfree(k_xfers);
-
return status;
-
}
说明:
1) 申请传输的结构体内存。
2) 通过for循环,对spi_ioc_transfer类型的数据进行转换。
3) 转换中,首先对本次传输的数据大小进行累计,如果总传输量超出buf的大小,直接退出。
4) 如果本次传输是接收,则设置接收数组,并对buf进行检查,查看是否可读。
5) 如果本次传输是写,则从应用层拷贝数据。
6) 对传输中参数进行赋值,包括速度、模式、速率等等。
7) 调用spidev_sync进行数据传输。
8) 将接收数据拷贝到应用层。
三、应用层
在应用层提供了二中驱动的测试程序,在“/documenation/spi”目录下,文件名称为spidev_test.c中,具体程序如下:
-
int main(int argc, char *argv[])
-
{
-
int ret = 0;
-
int fd;
-
-
parse_opts(argc, argv);
-
-
fd = open(device, O_RDWR);
-
if (fd < 0)
-
pabort("can't open device");
-
-
/*
-
* spi mode
-
*/
-
ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
-
if (ret == -1)
-
pabort("can't set spi mode");
-
-
ret = ioctl(fd, SPI_IOC_RD_MODE, &mode);
-
if (ret == -1)
-
pabort("can't get spi mode");
-
-
/*
-
* bits per word
-
*/
-
ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
-
if (ret == -1)
-
pabort("can't set bits per word");
-
-
ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
-
if (ret == -1)
-
pabort("can't get bits per word");
-
-
/*
-
* max speed hz
-
*/
-
ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
-
if (ret == -1)
-
pabort("can't set max speed hz");
-
-
ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
-
if (ret == -1)
-
pabort("can't get max speed hz");
-
-
printf("spi mode: %d\n", mode);
-
printf("bits per word: %d\n", bits);
-
printf("max speed: %d Hz (%d KHz)\n", speed, speed/1000);
-
-
transfer(fd);
-
-
close(fd);
-
-
return ret;
-
}
说明:
1) 首先打开设备。
2) 然后设置工作模式、位大小和速率等。
3) 传输数据
|