說明:
開發環境: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 <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kfifo.h>
#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中。想說的差不多就是這些,最後驗證一下這個驅動的基本功能。
Makefile
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文件在上一篇文章中進行過講述,這裏主要修改了倒數第二行的目標文件名。
測試
-
將驅動編譯並安裝到根目錄下
-
登陸開發板安裝驅動
-
查看相應的設備號
可以看到動態分配了251這個號。 -
在/dev目錄下創建vser0的設備節點:
mknod /dev/ser0 c 251 0
,第一個參數是節點名稱,c代表的是字符設備,251代表主設備號,0代表次設備號。mknod [-m MODE] NAME TYPE MAJOR MINOR
-
向虛擬串口ser0寫數據:
echo "hello world!" > /dev/vser0
-
從虛擬串口設備讀取數據:
cat /dev/vser0
可以看到從虛擬串口讀取了存進去的數據。說明一個簡單的具有迴環功能的虛擬串口驅動編寫成功了。可以進行下一環節的學習了。