linux字符設備驅(qū)動基本框架
掃描二維碼
隨時隨地手機看文章
對于Linux的驅(qū)動程序,需要遵循一定的框架結(jié)構(gòu)。嵌入式Linux的學習其實并不難,只要深入理解Linux的框架,寫起來也可以得心應手。
1.linux函數(shù)調(diào)用過程
1.1 系統(tǒng)函數(shù)調(diào)用的意義
在Linux的中,有一個思想比較重要:一切皆文件。
也就是說,在應用程序中,可以通過open,write,read等函數(shù)來操作底層的驅(qū)動。
比如操作led,函數(shù)如下
//點燈 fd1 = open("/dev/led",O_RDWR);
write(fd1,&val,4); //寫文本文件 fd2 = open("hello.txt",O_RDWR)
write(fd2,&val,4);
一般的,進程是不能訪問內(nèi)核的。它不能訪問內(nèi)核所占內(nèi)存空間也不能調(diào)用內(nèi)核函數(shù)。CPU硬件決定了這些(這就是為什么它被稱作"保護模式")。為了和用戶空間上執(zhí)行的進程進行交互,內(nèi)核提供了一組接口。透過該接口,應用程序能夠訪問問硬件設備和其它操作系統(tǒng)資源。這組接口在應用程序和內(nèi)核之間扮演了使者的角色,應用程序發(fā)送各種請求。而內(nèi)核負責滿足這些請求(或者讓應用程序臨時擱置)。
實際上提供這組接口主要是為了保證系統(tǒng)穩(wěn)定可靠。避免應用程序肆意妄行,惹出大麻煩。
下面是printf()串口打印調(diào)用的過程。

1.2 系統(tǒng)函數(shù)的調(diào)用過程
當應用程序調(diào)用open,read,write等函數(shù)時,最終會調(diào)用驅(qū)動中的fopen,fwrite,fread等函數(shù)。其過程如下
1.當應用程序調(diào)用open,read,ioctl等函數(shù)(C庫)時,會觸發(fā)一個系統(tǒng)異常SWI。
2.當觸發(fā)異常時,會進入到內(nèi)核系統(tǒng)調(diào)用接口(system call interface),會調(diào)用sys_open,sys_read,sys_write。
3.然后會進入虛擬文件系統(tǒng)(VFS)virtual filesystem。
4.最后進入到驅(qū)動函數(shù)的open,read,write函數(shù),read函數(shù)的本質(zhì)就是copy_to_user,而write函數(shù)就是copy_from_user。

1.3 用戶空間與內(nèi)核空間
Linux的操作系統(tǒng)分為內(nèi)核態(tài)和用戶態(tài),內(nèi)核態(tài)完成與硬件的交互,比如讀寫內(nèi)存,硬件操作等。用戶態(tài)運行上層的程序,比如Qt等。分成這兩種狀態(tài)的原因是即使應用程序出現(xiàn)異常,也不會使操作系統(tǒng)崩潰。
值得注意的是,用戶態(tài)和內(nèi)核態(tài)是可以互相轉(zhuǎn)換的。每當應用程序執(zhí)行系統(tǒng)調(diào)用或者被硬件中斷掛起時,Linux操作系統(tǒng)都會從用戶態(tài)切換到內(nèi)核態(tài);當系統(tǒng)調(diào)用完成或者中斷處理完成后,操作系統(tǒng)會從內(nèi)核態(tài)返回到用戶態(tài),繼續(xù)執(zhí)行應用程序。
2.驅(qū)動程序的框架
在理解設備框架之前,首先要知道驅(qū)動程序主要做了以下幾件事
1.將此內(nèi)核驅(qū)動模塊加載到內(nèi)核中
2.從內(nèi)核中將驅(qū)動模塊卸載
3.聲明遵循的開源協(xié)議
2.1 Linux下的設備
Linux下分成三大類設備:
字符設備:字符設備是能夠像字節(jié)流一樣被訪問的設備。一般來說對硬件的IO操作可歸結(jié)為字符設備。常見的字符設備有l(wèi)ed,蜂鳴器,串口,鍵盤等等。包括lcd與攝像頭驅(qū)動都屬于字符設備驅(qū)動。
塊設備:塊設備是通過內(nèi)存緩存區(qū)訪問,可以隨機存取的設備,一般理解就是存儲介質(zhì)類的設備,常見的字符設備有U盤,TF卡,eMMC,電腦硬盤,光盤等等
網(wǎng)絡設備:可以和其他主機交換數(shù)據(jù)的設備,主要有以太網(wǎng)設備,wifi,藍牙等。
字符設備與塊設備驅(qū)動程序的區(qū)別與聯(lián)系
1.字符設備的最小訪問單元是字節(jié),塊設備是塊字節(jié)512或者512字節(jié)為單位
2.訪問順序上面,字符設備是順序訪問的,而塊設備是隨機訪問的
3.在linux中,字符設備和塊設備訪問字節(jié)沒有本質(zhì)區(qū)別
網(wǎng)絡設備驅(qū)動程序的本質(zhì)
提供了協(xié)議與設備驅(qū)動通信的通用接口。
簡單的說,對于字符設備驅(qū)動就是可以按照先后順序訪問,不能隨機訪問,比如LCD,camera,UART等等,這些是字符設備的代表。對于I2C也劃分為字符設備驅(qū)動程序,也可以細分為總線設備驅(qū)動程序。塊設備驅(qū)動程序就是可以隨機訪問的緩沖區(qū)。
2.2 驅(qū)動程序框架的一個例子
對于一個驅(qū)動程序,如果想讓內(nèi)核知道,就準守一定的框架,下面來看一下一個最簡單的驅(qū)動程序的框架
#include #include //驅(qū)動程序入口函數(shù) static int test_init(void) {
printk("---Add---\n"); return 0;
} //驅(qū)動函數(shù)出口函數(shù) static void test_exit(void) {
printk("---Remove---\n");
} //告訴內(nèi)核,入口函數(shù) module_init(test_init); //告訴內(nèi)核,出口函數(shù) module_exit(test_exit);
MODULE_LICENSE("GPL"); //GPL GNU General Public License MODULE_AUTHOR("ZFJ"); //作者
如果要將上面的源碼編譯成驅(qū)動程序,還需要寫Makefile程序
obj-m:=test.o KDIR:=/lib/modules/$(shell uname -r)/build PWD:=$(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions *.order *symvers *Module.markers
其中需要解釋一下的是
$(MAKE) -C $(KDIR) M=$(PWD) modules
該命令是make modules命令的擴展,-C選項的作用是指將當前的工作目錄轉(zhuǎn)移到指定目錄,即(KDIR)目錄,程序到(shell pwd)當前目錄查找模塊源碼,將其編譯,生成.ko文件。
生成的.ko文件就是驅(qū)動程序,如果要將當前的驅(qū)動程序插入到內(nèi)核中,可以在控制臺輸入
sudo insmod test.ko
該命令會執(zhí)行test_init函數(shù)。如果要查看內(nèi)核打印信息,可輸入dmesg。用lsmod可查看目前掛載的驅(qū)動程序。
如果要移除當前的驅(qū)動程序,可調(diào)用
sudo rmmod test
該函數(shù)會執(zhí)行test_exit函數(shù)。
3.字符設備驅(qū)動程序解析
字符設備在Linux驅(qū)動中起到十分關(guān)鍵的作用。包括我們要實現(xiàn)的LCD驅(qū)動以及CAM驅(qū)動都屬于字符設備驅(qū)動。所以現(xiàn)在主要分析一下字符設備驅(qū)動程序的框架。
3.1 基本概念
對于了解字符設備驅(qū)動程序,需要知道的問題
(1)應用程序、庫、內(nèi)核、驅(qū)動程序的關(guān)系
應用程序調(diào)用函數(shù)庫,通過文件的操作完成一系列的功能。作為Linux特有的抽象方式,將所有的硬件抽象成文件的讀寫。
(2)設備類型
字符設備、塊設備、網(wǎng)絡設備
(3)設備文件、主設備號、從設備號
有了設備類型的劃分,還需要進行進一步明確。所以驅(qū)動設備會生成字符節(jié)點,以文件的方式存放在/dev目錄下,操作時可抽象成文件操作即可。每個設備節(jié)點有主設備號和次設備號,用一個32位來表示,前12位表示主設備號,后20位表示次設備號。例如"/dev/fb0","/dev/fb1"或者"/dev/tty1","/dev/tty2"等等。
3.2 創(chuàng)建流程
第一步:寫出驅(qū)動程序的框架
前面在創(chuàng)建驅(qū)動程序的框架時,只是測試了安裝與卸載驅(qū)動,并且找到驅(qū)動程序的入口與出口。并沒有一個字符設備操作的接口。作為一個字符設備驅(qū)動程序,其open,read,write等函數(shù)是必要的。但是最開始還是要實現(xiàn)一個驅(qū)動程序的入口與出口函數(shù)。
#include #include static int __init dev_fifo_init() { return 0;
} static void __exit dev_fifo_exit() {
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);
MODULE_LICENSE("Dual DSB/GPL");
MODULE_AUTHOR("ZHAO");
第二步:在驅(qū)動入口函數(shù)中申請設備號
一個字符設備或者塊設備都有一個主設備號和次設備號。主設備號和次設備號統(tǒng)稱為設備號。主設備號用來表示一個特定的驅(qū)動程序。次設備號用來表示使用該驅(qū)動程序的各設備。
//設備號 : 主設備號(12bit) | 次設備號(20bit) dev_num = MKDEV(MAJOR_NUM, 0); //靜態(tài)注冊設備號 ret = register_chrdev_region(dev_num,1,"dev_fifo"); if(ret < 0)
{ //靜態(tài)注冊失敗,進行動態(tài)注冊設備號 ret = alloc_chrdev_region(&dev_num,0,1,"dev_fifo"); if(ret < 0)
{
printk("Fail to register_chrdev_region\n"); goto err_register_chrdev_region;
}
}
靜態(tài)分設備號的函數(shù)原型
register_chrdev_region(dev_t first,unsigned int count,char *name)
1:第一個參數(shù):要分配的設備編號范圍的初始值, 這組連續(xù)設備號的起始設備號, 相當于register_chrdev()中主設備號
2:第二個參數(shù):連續(xù)編號范圍. 是這組設備號的大?。ㄒ彩谴卧O備號的個數(shù))
3:第三個參數(shù):編號相關(guān)聯(lián)的設備名稱. (/proc/devices); 本組設備的驅(qū)動名稱
其中動態(tài)分配的函數(shù)原型
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
1:這個函數(shù)的第一個參數(shù),是輸出型參數(shù),獲得一個分配到的設備號??梢杂肕AJOR宏和MINOR宏,將主設備號和次設備號,提取打印出來,看是自動分配的是多少,方便我們在mknod創(chuàng)建設備文件時用到主設備號和次設備號。 mknod /dev/xxx c 主設備號 次設備號
2:第二個參數(shù):次設備號的基準,從第幾個次設備號開始分配。
3:第三個參數(shù):次設備號的個數(shù)。
4:第四個參數(shù):驅(qū)動的名字
由于每個設備只有一個主設備號,所以如果用靜態(tài)分配設備號時,有可能會導致分配不成功,所以采用動態(tài)分配的方式。
注意,在入口函數(shù)中注冊,那么一定要記得在驅(qū)動出口函數(shù)中釋放
//釋放申請的設備號 unregister_chrdev_region(dev_num, 1);
第三步:創(chuàng)建設備類
這一步會在/sys/class/dev_fifo下創(chuàng)建接口
sysfs 文件系統(tǒng)總是被掛載在 /sys 掛載點上。雖然在較早期的2.6內(nèi)核系統(tǒng)上并沒有規(guī)定 sysfs 的標準掛載位置,可以把 sysfs 掛載在任何位置,但較近的2.6內(nèi)核修正了這一規(guī)則,要求 sysfs 總是掛載在 /sys 目錄上。
//創(chuàng)建設備類 cls = class_create(THIS_MODULE, "dev_fifo"); if(IS_ERR(cls))
{
ret = PTR_ERR(cls); goto err_class_create;
}
第四步:初始化字符設備
在這一步中,會初始化一個重要的結(jié)構(gòu)體,file_operations。
//初始化字符設備 cdev_init(&gcd->cdev,&fifo_operations);
該函數(shù)的原型為
cdev_init(struct cdev *cdev, const struct file_operations *fops)
第一個參數(shù)時字符設備結(jié)構(gòu)體,第二個參數(shù)為操作函數(shù)
Linux使用file_operations結(jié)構(gòu)訪問驅(qū)動程序的函數(shù),這個結(jié)構(gòu)的每一個成員的名字都對應著一個調(diào)用。
用戶進程利用在對設備文件進行諸如read/write操作的時候,系統(tǒng)調(diào)用通過設備文件的主設備號找到相應的設備驅(qū)動程序,然后讀取這個數(shù)據(jù)結(jié)構(gòu)相應的函數(shù)指針,接著把控制權(quán)交給該函數(shù),這是Linux的設備驅(qū)動程序工作的基本原理。
通常來說,字符設備驅(qū)動程序經(jīng)常用到的5種操作
struct file_operations { ssize_t (*read)(struct file *,char *, size_t, loff_t *);//從設備同步讀取數(shù)據(jù) ssize_t (*write)(struct file *,const char *, size_t, loff_t *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);//執(zhí)行設備IO控制命令 int (*open) (struct inode *, struct file *);//打開 int (*release)(struct inode *, struct file *);//關(guān)閉 };
第五步:添加設備到用戶操作系統(tǒng)
//添加設備到操作系統(tǒng) ret = cdev_add(&gcd->cdev,dev_num,1); if (ret < 0)
{ goto err_cdev_add;
}
函數(shù)原型為
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
第一個參數(shù)為cdev 結(jié)構(gòu)的指針
第二個參數(shù)為設備起始編號
第三個參數(shù)為設備編號范圍
這一步的含義在于將字符設備驅(qū)動加入到操作系統(tǒng)的驅(qū)動數(shù)組中。當應用程序調(diào)用open函數(shù)時,會首先找到該設備的設備號,然后根據(jù)這個設備號找到相應file_operations。調(diào)用其中的open以及讀寫函數(shù)。
第六步:導出設備信息到用戶空間
//導出設備信息到用戶空間(/sys/class/類名/設備名) device = device_create(cls,NULL,dev_num,NULL,"dev_fifo%d",0); if(IS_ERR(device)){
ret = PTR_ERR(device);
printk("Fail to device_create\n"); goto err_device_create;
}
函數(shù)原型
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
第一個參數(shù):struct class 指針,必須在本函數(shù)調(diào)用之前先被創(chuàng)建
第二個參數(shù):該設備的parent指針。
第三個參數(shù):字符設備的設備號,如果dev_t不是0,0的話,1個”dev”文件將被創(chuàng)建。
第四個參數(shù):被添加到該設備回調(diào)的數(shù)據(jù)。
第五個參數(shù):設備名字。
之前寫的字符類設備驅(qū)動,沒有自動創(chuàng)建設備節(jié)點,因為只使用了register_chrdev()函數(shù),只是注冊了這個設備。然后在系統(tǒng)啟動后,就要自己創(chuàng)建設備節(jié)點mknod,這樣雖然是可行的,但是比較麻煩。于是想在init函數(shù)里面,自動創(chuàng)建設備節(jié)點。
創(chuàng)建設備節(jié)點使用了兩個函數(shù) class_create()和class_device_create(),當然在exit()函數(shù)里,要使用class_destory()和class_device_desotry()注銷創(chuàng)建的設備節(jié)點!。
需要注意的是要使用該函數(shù)自動生成節(jié)點,內(nèi)核版本至少在Linux2.6.32 。
到這里,一個字符設備驅(qū)動程序的基本流程就完成了。編譯好驅(qū)動程序,然后安裝到Linux中,用insmod加載模塊。可以在/dev/dev_fifo0看到自己創(chuàng)建的設備節(jié)點。相關(guān)源代碼可參考附錄。
4. 總結(jié)
Linux將所有的設備都抽象成文件,這樣的操作接口比較的統(tǒng)一,也給開發(fā)帶來很大的方便。通過將寫好的驅(qū)動程序裝載到內(nèi)核可見的區(qū)域,使得內(nèi)核感知到模塊的存在,然后用戶空間才能通過系統(tǒng)調(diào)用聯(lián)系到驅(qū)動,從而完成它的任務。
寫驅(qū)動程序需要按照一定的步驟,首先申明驅(qū)動的入口和出口,然后注冊設備號。接著填充file_operations結(jié)構(gòu)體。引用程序通過調(diào)用open,read,或者write函數(shù),最終調(diào)用到file_operations的open,read或者write函數(shù),從而實現(xiàn)了從應用層到內(nèi)核層的調(diào)用。
附錄:程序代碼
#include #include #include #include #include #include #include //指定的主設備號 #define MAJOR_NUM 250 //自己的字符設備 struct mycdev { int len; unsigned char buffer[50]; struct cdev cdev; };
MODULE_LICENSE("GPL"); //設備號 static dev_t dev_num = {0}; //全局gcd struct mycdev *gcd; //設備類 struct class *cls; //打開設備 static int dev_fifo_open(struct inode *inode, struct file *file) {
printk("dev_fifo_open success!\n"); return 0;
} //讀設備 static ssize_t dev_fifo_read(struct file *file, char __user *ubuf, size_t size, loff_t *ppos) { int n; int ret; char *kbuf;
printk("read *ppos : %lld\n",*ppos); if(*ppos == gcd->len) return 0; //請求大大小 > buffer剩余的字節(jié)數(shù) :讀取實際記得字節(jié)數(shù) if(size > gcd->len - *ppos)
n = gcd->len - *ppos; else n = size;
printk("n = %d\n",n); //從上一次文件位置指針的位置開始讀取數(shù)據(jù) kbuf = gcd->buffer + *ppos; //拷貝數(shù)據(jù)到用戶空間 ret = copy_to_user(ubuf,kbuf, n); if(ret != 0) return -EFAULT; //更新文件位置指針的值 *ppos += n;
printk("dev_fifo_read success!\n"); return n;
} //寫設備 static ssize_t dev_fifo_write(struct file *file, const char __user *ubuf, size_t size, loff_t *ppos) { int n; int ret; char *kbuf;
printk("write *ppos : %lld\n",*ppos); //已經(jīng)到達buffer尾部了 if(*ppos == sizeof(gcd->buffer)) return -1; //請求大大小 > buffer剩余的字節(jié)數(shù)(有多少空間就寫多少數(shù)據(jù)) if(size > sizeof(gcd->buffer) - *ppos)
n = sizeof(gcd->buffer) - *ppos; else n = size; //從上一次文件位置指針的位置開始寫入數(shù)據(jù) kbuf = gcd->buffer + *ppos; //拷貝數(shù)據(jù)到內(nèi)核空間 ret = copy_from_user(kbuf, ubuf, n); if(ret != 0) return -EFAULT; //更新文件位置指針的值 *ppos += n; //更新dev_fifo.len gcd->len += n;
printk("dev_fifo_write success!\n"); return n;
} //設備操作函數(shù)接口 static const struct file_operations fifo_operations = { .owner = THIS_MODULE,
.open = dev_fifo_open,
.read = dev_fifo_read,
.write = dev_fifo_write,
}; //模塊入口 int __init dev_fifo_init(void) { int ret; struct device *device; //動態(tài)申請內(nèi)存 gcd = kzalloc(sizeof(struct mycdev), GFP_KERNEL); if(!gcd){ return -ENOMEM;
} //設備號 : 主設備號(12bit) | 次設備號(20bit) dev_num = MKDEV(MAJOR_NUM, 0); //靜態(tài)注冊設備號 ret = register_chrdev_region(dev_num,1,"dev_fifo"); if(ret < 0){ //靜態(tài)注冊失敗,進行動態(tài)注冊設備號 ret = alloc_chrdev_region(&dev_num,0,1,"dev_fifo"); if(ret < 0){
printk("Fail to register_chrdev_region\n"); goto err_register_chrdev_region;
}
} //創(chuàng)建設備類 cls = class_create(THIS_MODULE, "dev_fifo"); if(IS_ERR(cls)){
ret = PTR_ERR(cls); goto err_class_create;
} //初始化字符設備 cdev_init(&gcd->cdev,&fifo_operations); //添加設備到操作系統(tǒng) ret = cdev_add(&gcd->cdev,dev_num,1); if (ret < 0)
{ goto err_cdev_add;
} //導出設備信息到用戶空間(/sys/class/類名/設備名) device = device_create(cls,NULL,dev_num,NULL,"dev_fifo%d",0); if(IS_ERR(device)){
ret = PTR_ERR(device);
printk("Fail to device_create\n"); goto err_device_create;
}
printk("Register dev_fito to system,ok!\n"); return 0;
err_device_create:
cdev_del(&gcd->cdev);
err_cdev_add:
class_destroy(cls);
err_class_create:
unregister_chrdev_region(dev_num, 1);
err_register_chrdev_region: return ret;
} void __exit dev_fifo_exit(void) { //刪除sysfs文件系統(tǒng)中的設備 device_destroy(cls,dev_num ); //刪除系統(tǒng)中的設備類 class_destroy(cls); //從系統(tǒng)中刪除添加的字符設備 cdev_del(&gcd->cdev); //釋放申請的設備號 unregister_chrdev_region(dev_num, 1); return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);
MODULE_LICENSE("Dual DSB/GPL");
MODULE_AUTHOR("ZHAO");