说明:
开发环境:ubuntu14.04
硬件环境:EasyArm-i.mx283A
这里的虚拟串口设备并不是开发板上的外设,而是由驱动模拟出来的一个fifo缓冲区,在功能上类似串口外设,可以实现数据的收发,收发对象是用户层和内核层,在一定程度上可以说是弱化的具有内环回作用的串口。当用户在命令行通过echo命令向设备写入数据时,同时也可以通过命令cat从设备读数据,而且写入的数据和读出的数据完全一样。驱动中为实现这一功能,主要用到以下三个宏:
include/linux/kfifo.h:
DEFINE_KFIFO(name, size)
int kfifo_from_user(struct kfifo *fifo,const void __user *from, unsigned int n,unsigned *lenout);
int kfifo_to_user(struct kfifo *fifo,void __user *to, unsigned int n, unsigned
*lenout);
DEFINE_KFIFO用于定义一个大小为size,名为name的一个fifo缓冲区,必须注意的是size一定为2的幂,比如2,4,8,16,32。kfifo_from_user用于将用户空间的数据存放入fifo中,from是用户使用的buf,n是buf的大小,lenout是实际存入fifo的元素个数。与之相反的是,kfifo_to_user是从fifo中取数据到from中,然后复制到用户空间,n指定from的大小,实际取出的数据由lenout指定。
虚拟串口设备属于字符设备,因此编写虚拟串口设备按照字符设备驱动框架来,注册设备号—>
初始化cdev—>将cdev添加到cdev_map散列表中—>删除cdev—>注销设备号。框架搭建好之后,将编写驱动程序真正灵活的地方,那就是操作方法的编写,特别是读操作和写操作的编写,编写者应该站在内核的角度来思考数据的流向。对于虚拟设备而言,文件的打开和关闭操作比较简单,内部可以不用做任何操作,直接返回0就好了。好了,废言少吐,直接上程序!
#include
#include
#include
#include
#include
#include
#define VSER_CNT 1 //一个虚拟设备
#define VSER_NAME "vser" //虚拟设备的名称
static struct cdev vsdev_s;//静态分配一个cdev
DEFINE_KFIFO(vsfifo, 32);//分配一个fifo缓冲区
dev_t dev;//动态分配设备号
static int vser_open(struct inode *inode, struct file *filp)
{
return 0;
}
static int vser_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t vser_read(struct file *flip, char __user *buf, size_t count, loff_t *pos)
{
unsigned int copied = 0;
kfifo_to_user(&vsfifo,buf, count, &copied);
printk("数据发送完成!\n");
return copied;
}
static ssize_t vser_write(struct file *flip, const char __user *buf, size_t count, loff_t *pos)
{
unsigned int copied = 0;
kfifo_from_user(&vsfifo,buf,count,&copied);
printk("数据接收完成!\n");
return copied;
}
//虚拟串口操作方法集合
static struct file_operations vsdev_ops={
.owner = THIS_MODULE,
.open = vser_open,
.write = vser_write,
.read = vser_read,
.release = vser_release,
};
static int __init vser_init(void)
{
int ret = 0;//返回值
ret = alloc_chrdev_region(&dev,0,VSER_CNT,VSER_NAME);
if(ret)
{
goto reg_err;
}
cdev_init(&vsdev_s,&vsdev_ops);//初始化cdev
vsdev_s.owner = THIS_MODULE;
ret = cdev_add(&vsdev_s,dev,VSER_CNT);
if(ret)
{
goto add_err;
}
printk("模块加载成功!\n");
return 0;
add_err:
unregister_chrdev_region(dev,VSER_CNT);
reg_err:
return ret;
}
static void __exit vser_exit(void)
{
cdev_del(&vsdev_s);
unregister_chrdev_region(dev,VSER_CNT);
printk("模块卸载成功!\n");
}
module_init(vser_init);
module_exit(vser_exit);
//模块说明
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YangZhengQing <[email protected]>");
MODULE_DESCRIPTION("Virtual serial port device");
驱动程序阅读顺序,目前我也不知道我的读法是否正确,我一般习惯从初始化函数vser_init
开始阅读,然后读卸载函数vser_exit
,最后对着操作方法集file_operations
慢慢斟酌每一个方法,为什么要用斟酌二字呢,因为我还是一名自豪的菜鸟呀,等将来某一天我变成了老鸟了,哼哼,相信到时候阅读内核源码、驱动程序就会像砍小白菜一样easy。
这个驱动程序比较简单,没有必要从头分析到尾,我只想分析一下我觉得比较重要的东西,以便加深记忆。vser_init
初始化函数中调用alloc_chrdev_region(&dev,0,VSER_CNT,VSER_NAME);
动态分配设备号,光看程序我们无法获知具体的设备号,模块加载成功后,不能直接使用mknod命令安装设备节点,应该事先使用命令cat /proc/devices
查看对应设备的设备号,然后再mknod。另外,用于保存设备号的变量dev应该设置为全局变量,因为后面的vser_exit
卸载函数中需要用到被动态分配好的设备号。
file_operations
结构体中有五个成员,除第一个owner外,其他四个是虚拟串口设备的操作方法,上层应用只有历尽千辛万苦找到这些操作接口才能真正成功地操作硬件设备,但是这里并不是真正的硬件设备,而是虚拟的,所以对于vser_open
和vser_release
没有必要实现具体的文件打开和关闭的操作。再有,open对应的应该是close,而驱动程序中open对应的确实release,这样命名有何用意呢?原因是一个文件可以被打开多次,只有close到最后一个文件时,release才会被调用,所以命名为release,"释放"之意更为贴切。
vser_read
函数中调用了kfifo_to_user
,这里需要进行角色的变换,对于上层应用来说是read,对于内核来说是write,所以这里用了kfifo_to_user,将fifo缓冲区的数据写向用户层。而在vser_erite
函数中调用的是kfifo_from_user
,对于上层应用来说write,对于内核来说是read,所以这里调用的是kfifo_from_user,意思是从用户空间取数据存放入fifo中。想说的差不多就是这些,最后验证一下这个驱动的基本功能。
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR := /home/yzq/EasyArm-imx28xx/kernel/linux-2.6.35.3
ROOTFS := /nfsroot/rootfs
else
KERNELDIR := /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.ko *.order *.symvers *.cmd *.o *.mod.c *.tmp_versions .*.cmd .tmp_versions
else
obj-m := vser.o
endif
Makefile文件在上一篇文章中进行过讲述,这里主要修改了倒数第二行的目标文件名。