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
















 
            
             
             
                 
             工商網監(jiān)
工商網監(jiān)
        
評論