大部分學習者的最終目的就是學習 Linux驅動開發,Linux中的外設驅動可以分為:字符設備驅動、塊設備驅動和網絡設備驅動。
|?字符設備驅動簡介
字符設備是 Linux 驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先后順序的。比如最常見的點燈、按鍵、IIC、SPI,LCD 等等都是字符設備,這些設備的驅動就叫做字符設備驅動。
Linux 應用程序對驅動程序的調用流程
在 Linux 中一切皆為文件,驅動加載成功以后會在“/dev”目錄下生成一個相應的文件,應用程序通過對這個名為“/dev/xxx”(xxx 是具體的驅動文件名字)的文件進行相應的操作即可實現對硬件的操作。 比如現在有個叫做/dev/led 的驅動文件,此文件是 led 燈的驅動文件。應用程序使用 open 函數來打開文件/dev/led,使用完成以后使用 close 函數關閉/dev/led 這個文件。open和 close 就是打開和關閉 led 驅動的函數,如果要點亮或關閉 led,那么就使用 write 函數來操作,也就是向此驅動寫入數據,這個數據就是要關閉還是要打開 led 的控制參數。如果要獲取led 燈的狀態,就用 read 函數從驅動中讀取相應的狀態。
應用程序運行在用戶空間,而Linux驅動屬于內核的一部分,因此驅動運行于內核空間。當應用層通過open函數打開/dev/led 這個驅動時,因用戶空間不能直接操作內核,因此會使用“系統調用”的方法來從用戶空間“陷入”到內核空間,實現對底層驅動的操作。
比如應用程序調用了open這個函數,則在驅動程序中也應有一個對應的open的函數。
Linux內核驅動操作函數
每一個系統調用,在驅動中都有與之對應的一個驅動函數,在 Linux 內核文件 include/linux/fs.h 中有個叫做 file_operations 的結構體,此結構體就是 Linux 內核驅動操作函數集合:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, constchar __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsignedint, unsignedlong); long (*compat_ioctl) (struct file *, unsignedint, unsignedlong); int (*mmap) (struct file *, struct vm_area_struct *); int (*mremap)(struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); /*省略若干行...*/ };其中常用的函數有以下這幾種:
?
owner:擁有該結構體的模塊的指針,一般設置為THIS_MODULE。
llseek函數:用于修改文件當前的讀寫位置。
read函數:用于讀取設備文件。
write函數:用于向設備文件寫入(發送)數據。
poll函數:是個輪詢函數,用于查詢設備是否可以進行非阻塞的讀寫。
unlocked_ioctl函數:提供對于設備的控制功能, 與應用程序中的 ioctl 函數對應。
compat_ioctl函數:與 unlocked_ioctl功能一樣,區別在于在 64 位系統上,32 位的應用程序調用將會使用此函數。在 32 位的系統上運行 32 位的應用程序調用的是unlocked_ioctl。
mmap函數:用于將將設備的內存映射到進程空間中(也就是用戶空間),一般幀緩沖設備會使用此函數, 比如 LCD 驅動的顯存,將幀緩沖(LCD 顯存)映射到用戶空間中以后應用程序就可以直接操作顯存了,這樣就不用在用戶空間和內核空間之間來回復制。
open函數:用于打開設備文件。
release函數:用于釋放(關閉)設備文件,與應用程序中的 close 函數對應。
fasync函數:用于刷新待處理的數據,用于將緩沖區中的數據刷新到磁盤中。
aio_fsync函數:與fasync功能類似,只是 aio_fsync 是異步刷新待處理的
Linux 驅動有兩種運行方式
Linux 驅動有兩種運行方式,第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啟動的時候就會自動運行驅動程序。第二種就是將驅動編譯成模塊(Linux 下模塊擴展名為.ko),在Linux 內核啟動以后使用“insmod”命令加載驅動模塊。在驅動開發階段一般都將其編譯為模塊,不需要編譯整個Linux代碼,方便調試驅動程序。當驅動開發完成后,根據實際需要,可以選擇是否將驅動編譯進Linux內核中。 Linux 設備號 為了方便管理,Linux 中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成,主設備號表示某一個具體的驅動,次設備號表示使用這個驅動的各個設備。Linux 提供了一個名為 dev_t 的數據類型表示設備號,dev_t 定義在文件 include/linux/types.h 里面,定義如下:
typedef __u32 __kernel_dev_t; ...... typedef __kernel_dev_t dev_t;dev_t 其實就是 unsigned int 類型,是一個 32 位的數據類型。這 32 位的數據構成了主設備號和次設備號兩部分,其中高 12 位為主設備號,低 20 位為次設備號。因此 Linux系統中主設備號范圍為 0~4095,所以大家在選擇主設備號的時候一定不要超過這個范圍。 在文件 include/linux/kdev_t.h 中提供了幾個關于設備號的操作函數(本質是宏),如下所示:
#define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) 8 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
?
MINORBITS:表示次設備號位數,一共20位
MINORMASK:表示次設備號掩碼
MAJOR:用于從dev_t中獲取主設備號,將dev_t右移20位即可
MINOR:用于從dev_t中獲取次設備號,取dev_t的低20位的值即可
MKDEV:用于將給定的主設備號和次設備號的值組合成dev_t類型的設備號
設備號的分配有兩種方式,第一種是靜態分配設備號,需要開發者手動指定設備號,并且要注意不能與已有的重復,一些常用的設備號已經被Linux內核開發者給分配掉了,使用“cat /proc/devices”命令可查看當前系統中所有已經使用了的設備號。;第二種是動態分配設備號,靜態分配設備號很容易帶來沖突問題,Linux 社區推薦使用動態分配設備號,在注冊字符設備之前先申請一個設備號,系統會自動給你一個沒有被使用的設備號,這樣就避免了沖突。 設備號的申請函數
/* dev:保存申請到的設備號。 baseminor:次設備號起始地址,一般 baseminor 為?0,也就是說次設備號從?0?開始。 count:要申請的設備號數量。 name:設備名字。 */ int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)? 設備號釋放函數
/* from:要釋放的設備號。 count:表示從 from 開始,要釋放的設備號數量。 */ void unregister_chrdev_region(dev_t from, unsigned count)
?
| 字符設備驅動開發模板
加載與卸載
在編寫驅動的時候需要注冊模塊加載和卸載這兩種函數:
?
module_init(xxx_init); //注冊模塊加載函數 module_exit(xxx_exit); //注冊模塊卸載函數
?
module_init()用來向Linux內核注冊一個模塊加載函數,參數xxx_init就是需要注冊的具體函數,當使用 “insmod” 命令加載驅動的時候,xxx_init這個函數就會被調用。
module_exit()用來向Linux內核注冊一個模塊卸載函數,參數xxx_exit就是需要注冊的具體函數,當使用“rmmod”命令卸載具體驅動的時候 xxx_exit函數就會被調用。
字符設備驅動模塊加載和卸載模板如下所示:
?
/* 驅動入口函數 */ static int __init xxx_init(void) { /*入口函數內容 */ return0; } /* 驅動出口函數 */ static void __exit xxx_exit(void) { /*出口函數內容*/ } /*指定為驅動的入口和出口函數 */ module_init(xxx_init); module_exit(xxx_exit);
?
驅動編譯完成以后擴展名為.ko, 有兩種命令可以加載驅動模塊:
insmod:最簡單的模塊加載命令,用于加載指定的.ko模塊,此命令不能解決模塊的依賴關系
modprobe:該命令會分析模塊的依賴關系,將所有的依賴模塊都加載到內核中,因此更智能
modprobe 命令默認會去/lib/modules/
卸載驅動也有兩種命令:
rmmod:例如使用rmmod drv.ko來卸載 drv.ko這一個模塊
modprobe -r:該命令除了卸載指定的驅動,還卸載其所依賴的其他模塊,若這些依賴模塊還在被其它模塊使用,就不能使用 modprobe來卸載驅動模塊! ?
注冊與注銷
對于字符設備驅動而言,當驅動模塊加載成功以后需要注冊字符設備,同樣,卸載驅動模塊的時候也需要注銷掉字符設備。
字符設備的注冊函數原型如下所示:
?
/* major:主設備號 name:設備名字,指向一串字符串 fops:結構體 file_operations 類型指針,指向設備的操作函數集合變量 */ static?inline?int?register_chrdev(unsigned?int?major,?const?char?*name,const?struct?file_operations?*fops)
?
字符設備的注銷函數原型如下所示:
?
/* major:要注銷的設備對應的主設備號。 name:要注銷的設備對應的設備名。 */ static inline void unregister_chrdev(unsigned int major, const char *name)
?
一般字符設備的注冊在驅動模塊的入口函數?xxx_init 中進行,字符設備的注銷在驅動模塊的出口函數?xxx_exit 中進行。
?
staticstruct file_operations test_fops; /* 驅動入口函數 */ static int __init xxx_init(void) { ??/*?入口函數具體內容?*/ ??int?retvalue?=?0;? ??/*?注冊字符設備驅動?*/ ??retvalue?=?register_chrdev(200,?"chrtest",?&test_fops);? if(retvalue < 0) { /* 字符設備注冊失敗, 自行處理 */ } return0; } /* 驅動出口函數 */ static void __exit xxx_exit(void) { /* 注銷字符設備驅動 */ unregister_chrdev(200, "chrtest"); } /* 將上面兩個函數指定為驅動的入口和出口函數 */ module_init(xxx_init); module_exit(xxx_exit);要注意的一點就是,選擇沒有被使用的主設備號;
// 查看當前已經被使用掉的設備號 cat /proc/devices
?
實現設備的具體操作函數
file_operations 結構體就是設備的具體操作函數,在上圖代碼中定義了file_operations結構體類型的變量test_fops,但是還沒對其進行初始化,也就是初始化其中的 open、release、read 和 write 等具體的設備操作函數。
假設對chrtest這個設備有如下兩個要求:
能夠實現打開和關閉操作:需要實現 file_operations 中的open和release?這兩個函數
能夠實現進行讀寫操作:需要實現 file_operations 中的read和write這兩個函數
?
/*打開設備*/ static int chrtest_open(struct inode *inode, struct file *filp) { ??/*用戶實現具體功能*/ return0; } /*從設備讀取*/ static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { ??/*用戶實現具體功能*/ return0; } /*向設備寫數據*/ static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { ??/*用戶實現具體功能*/ return0; } /*關閉釋放設備*/ static int chrtest_release(struct inode *inode, struct file *filp) { ??/*用戶實現具體功能*/ return0; } /*文件操作結構體*/ staticstruct file_operations test_fops = { .owner = THIS_MODULE, .open = chrtest_open, .read = chrtest_read, .write = chrtest_write, .release = chrtest_release, }; /*驅動入口函數*/ static int __init xxx_init(void) { /*入口函數具體內容*/ int retvalue = 0; /*注冊字符設備驅動*/ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0) { /*字符設備注冊失敗*/ } return0; } /*驅動出口函數*/ static void __exit xxx_exit(void) { /*注銷字符設備驅動*/ unregister_chrdev(200, "chrtest"); } /*指定為驅動的入口和出口函數*/ module_init(xxx_init); module_exit(xxx_exit);
?
一開始編寫了四個函數:chrtest_open、chrtest_read、chrtest_write和 chrtest_release。這四個函數就是 chrtest 設備的 open、read、write 和 release 操作函數。結構體配置就是初始化 test_fops 的 open、read、write 和 release 這四個成員變量。
? 添加 LICENSE 和作者信息 最后需要在驅動中加入 LICENSE(許可) 信息和作者信息,其中 LICENSE 是必須添加的,否則的話編譯的時候會報錯,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下兩個函數:
MODULE_LICENSE() //添加模塊 LICENSE 信息 MODULE_AUTHOR() //添加模塊作者信息?
/* 打開設備 */ static int chrtest_open(struct inode *inode, struct file *filp) { /* 用戶實現具體功能 */ return 0; } ...... /* 將上面兩個函數指定為驅動的入口和出口函數 */ module_init(xxx_init); module_exit(xxx_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("zuozhongkai");? LICENSE 采用 GPL 協議,有時候協議是很有必要的,特別是開源的項目,對于常用的協議還是要有一定的了解。?
?
評論