目錄
1.字元裝置驅動簡介
2.字元裝置驅動開發步驟
1)驅動子產品的加載和解除安裝
2)字元裝置注冊與登出
3)實作裝置的具體操作函數
4)添加LICENSE和作者資訊
3.Linux裝置号
1)裝置号的組成
2)裝置号的配置設定
1、靜态配置設定裝置号
2、動态配置設定裝置号
4.chrdevbase字元裝置驅動開發實驗
1)實驗程式編寫
2)編寫測試APP
1、C庫檔案操作基本函數
2、編寫測試APP程式
3)編譯驅動程式和測試APP
1、編譯驅動程式
2、編譯測試APP
4)運作測試
1、加載驅動子產品
2、建立裝置節點檔案
本篇我們将會詳細講解Linux中的三大類驅動:字元裝置驅動、塊裝置驅動和網絡裝置驅動。其
中字元裝置驅動是占用篇幅最大的一類驅動,因為字元裝置最多,從最簡單的點燈到I2C、
SPI、音頻等都屬于字元裝置驅動的類型。
塊裝置和網絡裝置驅動要比字元裝置驅動複雜,就是因為其複雜是以半導體廠商一般都給我們
編寫好了,大多數情況下都是直接可以使用的。所謂的塊裝置驅動就是存儲器裝置的驅動,比
如EMMC、NAND、SD卡和U盤等儲存設備,因為這些儲存設備的特點是以存儲塊為基礎,因
此叫做塊裝置。
網絡裝置驅動就更好了解了,就是網絡驅動,不管是有線的還是無線的,都屬于網絡裝置驅動
的範疇。一個裝置可以屬于多種裝置驅動類型,比如USBWIFI,其使用USB接口,是以屬于字
符裝置,但是其又能上網,是以也屬于網絡裝置驅動。
本篇我們就圍繞着三大裝置驅動類型展開,盡可能詳細的講解每種裝置驅動的開發方式。本書
使用的Linux核心版本為4.1.15,其支援裝置樹(Devicetree),是以本篇所有例程均采用裝置
樹。裝置樹将是本篇的重點!從裝置樹的基本原理到裝置樹驅動的開發方式,從最簡單的點燈
到複雜的網絡驅動開發,本篇均有詳細的講解,是學習裝置樹的不二之選。
本章我們從 Linux 驅動開發中最基礎的字元裝置驅動開始,重點學習Linux下字元裝置驅
動開發架構。本章會以一個虛拟的裝置為例,講解如何進行字元裝置驅動開發,以及如何編寫
測試APP來測試驅動工作是否正常,為以後的學習打下堅實的基礎。
1.字元裝置驅動簡介
字元裝置是Linux驅動中最基本的一類裝置驅動,字元裝置就是一個一個位元組,按照位元組流進
行讀寫操作的裝置,讀寫資料是分先後順序的。比如我們最常見的點燈、按鍵、IIC、SPI,
LCD等等都是字元裝置,這些裝置的驅動就叫做字元裝置驅動。在詳細的學習字元裝置驅動
架構之前,我們先來簡單的了解一下Linux下的應用程式是如何調用驅動程式的,Linux應用程
序對驅動程式的調用如圖1所示:

圖1 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、close、write和read等這些函數是由
C庫提供的,在Linux系統中,系統調用作為C庫的一部分。當我們調用open函數的時候流程如
圖2所示:
圖2 open函數調用流程
其中關于C庫以及如何通過系統調用“陷入”到核心空間這個我們不用去管,我們重點關注的是
應用程式和具體的驅動,應用程式使用到的函數在具體驅動程式中都有與之對應的函數,比如
應用程式中調用了open這個函數,那麼在驅動程式中也得有一個名為open的函數。每一個系
統調用,在驅動中都有與之對應的一個驅動函數,在Linux核心檔案include/linux/fs.h中有個叫
做file_operations的結構體,此結構體就是Linux核心驅動操作函數集合,内容如下所示:
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1592 ssize_t (*write) (struct file *, const char __user *, size_t,
loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
1606 int (*fasync) (int, struct file *, int);
1607 int (*lock) (struct file *, int, struct file_lock *);
1608 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
1609 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
1610 int (*check_flags)(int);
1611 int (*flock) (struct file *, int, struct file_lock *);
1612 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
1613 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
1614 int (*setlease)(struct file *, long, struct file_lock **, void **);
1615 long (*fallocate)(struct file *file, int mode, loff_t offset,
1616 loff_t len);
1617 void (*show_fdinfo)(struct seq_file *m, struct file *f);
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };
簡單介紹一下file_operation結構體中比較重要的、常用的函數:
第1589行,owner擁有該結構體的子產品的指針,一般設定為THIS_MODULE。
第1590行,llseek函數用于修改檔案目前的讀寫位置。
第1591行,read函數用于讀取裝置檔案。
第1592行,write函數用于向裝置檔案寫入(發送)資料。
第1596行,poll是個輪詢函數,用于查詢裝置是否可以進行非阻塞的讀寫。
第1597行,unlocked_ioctl函數提供對于裝置的控制功能,與應用程式中的ioctl函數對應。
第1598行,compat_ioctl函數與unlocked_ioctl函數功能一樣,差別在于在64位系統上,32位
的應用程式調用将會使用此函數。在32位的系統上運作32位的應用程式調用的是
unlocked_ioctl。
第1599行,mmap函數用于将将裝置的記憶體映射到程序空間中(也就是使用者空間),一般幀緩沖
裝置會使用此函數,比如LCD驅動的顯存,将幀緩沖(LCD顯存)映射到使用者空間中
以後應用程式就可以直接操作顯存了,這樣就不用在使用者空間和核心空間之間來
回複制。
第1601行,open函數用于打開裝置檔案。
第1603行,release函數用于釋放(關閉)裝置檔案,與應用程式中的close函數對應。
第1604行,fasync函數用于重新整理待處理的資料,用于将緩沖區中的資料重新整理到磁盤中。
第1605行,aio_fsync函數與fasync函數的功能類似,隻是aio_fsync是異步重新整理待處理的數
據。
在字元裝置驅動開發中最常用的就是上面這些函數,關于其他的函數大家可以查閱相關文檔。
我們在字元裝置驅動開發中最主要的工作就是實作上面這些函數,不一定全部都要實作,但是
像open、release、write、read等都是需要實作的,當然了,具體需要實作哪些函數還是要看
具體的驅動要求。
2.字元裝置驅動開發步驟
上一小節我們簡單的介紹了一下字元裝置驅動,那麼字元裝置驅動開發都有哪些步驟呢?我們
在學習裸機或者STM32的時候關于驅動的開發就是初始化相應的外設寄存器,在Linux驅動開
發中肯定也是要初始化相應的外設寄存器,這個是毫無疑問的。隻是在Linux驅動開發中我們
需要按照其規定的架構來編寫驅動,是以說學Linux驅動開發重點是學習其驅動架構。
1)驅動子產品的加載和解除安裝
Linux驅動有兩種運作方式,第一種就是将驅動編譯進Linux核心中,這樣當Linux核心啟
動的時候就會自動運作驅動程式。第二種就是将驅動編譯成子產品(Linux下子產品擴充名
為.ko),在Linux核心啟動以後使用“insmod”指令加載驅動子產品。在調試驅動的時候一般都
選擇将其編譯為子產品,這樣我們修改驅動以後隻需要編譯一下驅動代碼即可,不需要編
譯整個Linux代碼。而且在調試的時候隻需要加載或者解除安裝驅動子產品即可,不需要重新開機整
個系統。總之,将驅動編譯為子產品最大的好處就是友善開發,當驅動開發完成,确定沒
有問題以後就可以将驅動編譯進Linux核心中,當然也可以不編譯進Linux核心中,具體
看自己的需求。
子產品有加載和解除安裝兩種操作,我們在編寫驅動的時候需要注冊這兩種操作函數,子產品的加
載和解除安裝注冊函數如下:
module_init(xxx_init); //注冊子產品加載函數
module_exit(xxx_exit); //注冊子產品解除安裝函數
module_init函數用來向Linux核心注冊一個子產品加載函數,參數xxx_init就是需要注冊的具
體函數,當使用“insmod”指令加載驅動的時候,xxx_init這個函數就會被調用。
module_exit()函數用來向Linux核心注冊一個子產品解除安裝函數,參數xxx_exit就是需要注冊
的具體函數,當使用“rmmod”指令解除安裝具體驅動的時候xxx_exit函數就會被調用。字元設
備驅動子產品加載和解除安裝模闆如下所示:
1 /* 驅動入口函數 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函數具體内容 */
5 return 0;
6 }
7
8 /* 驅動出口函數 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函數具體内容 */
12 }
13
14 /* 将上面兩個函數指定為驅動的入口和出口函數 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);
第2行,定義了個名為xxx_init的驅動入口函數,并且使用了“__init”來修飾。
第9行,定義了個名為xxx_exit的驅動出口函數,并且使用了“__exit”來修飾。
第15行,調用函數module_init來聲明xxx_init為驅動入口函數,當加載驅動的時候xxx_init
函數就會被調用。
第16行,調用函數module_exit來聲明xxx_exit為驅動出口函數,當解除安裝驅動的時候
xxx_exit函數就會被調用。
驅動編譯完成以後擴充名為.ko,有兩種指令可以加載驅動子產品:insmod和modprobe,
insmod是最簡單的子產品加載指令,此指令用于加載指定的.ko子產品,比如加載drv.ko這個
驅動子產品,指令如下:
insmod drv.ko
insmod指令不能解決子產品的依賴關系,比如drv.ko依賴first.ko這個子產品,就必須先使用
insmod指令加載first.ko這個子產品,然後再加載drv.ko這個子產品。但是modprobe就不會存
在這個問題,modprobe會分析子產品的依賴關系,然後會将所有的依賴子產品都加載到核心
中,是以modprobe指令相比insmod要智能一些。
modprobe指令主要智能在提供了子產品的依賴性分析、錯誤檢查、錯誤報告等功能,推薦
使用modprobe指令來加載驅動。modprobe指令預設會去/lib/modules/
目錄中查找子產品,比如本書使用的Linuxkernel的版本号為4.1.15,是以modprobe指令默
認會到/lib/modules/4.1.15這個目錄中查找相應的驅動子產品,一般自己制作的根檔案系統
中是不會有這個目錄的,是以需要自己手動建立。
驅動子產品的解除安裝使用指令“rm mod”即可,比如要解除安裝drv.ko,使用如下指令即可:
rmmod drv.ko
也可以使用“modprobe -r”指令解除安裝驅動,比如要解除安裝drv.ko,指令如下:
modprobe -r drv.ko
使用modprobe指令可以解除安裝掉驅動子產品所依賴的其他子產品,前提是這些依賴子產品已經沒
有被其他子產品所使用,否則就不能使用modprobe來解除安裝驅動子產品。是以對于子產品的卸
載,還是推薦使用rmmod指令。
2)字元裝置注冊與登出
對于字元裝置驅動而言,當驅動子產品加載成功以後需要注冊字元裝置,同樣,解除安裝驅動
子產品的時候也需要登出掉字元裝置。字元裝置的注冊和登出函數原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev函數用于注冊字元裝置,此函數一共有三個參數,這三個參數的含義如
下:
major:主裝置号,Linux下每個裝置都有一個裝置号,裝置号分為主裝置号和次裝置号
兩部分,關于裝置号後面會詳細講解。
name:裝置名字,指向一串字元串。
fops:結構體file_operations類型指針,指向裝置的操作函數集合變量。
unregister_chrdev函數使用者登出字元裝置,此函數有兩個參數,這兩個參數含義如下:
major:要登出的裝置對應的主裝置号。
name:要登出的裝置對應的裝置名。
一般字元裝置的注冊在驅動子產品的入口函數xxx_init中進行,字元裝置的登出在驅動子產品
的出口函數xxx_exit中進行。示例代碼如下所示:
1 static struct file_operations test_fops;
2
3 /* 驅動入口函數 */
4 static int __init xxx_init(void)
5 {
6 /* 入口函數具體内容 */
7 int retvalue = 0;
8
9 /* 注冊字元裝置驅動 */
10 retvalue = register_chrdev(200, "chrtest", &test_fops);
11 if(retvalue < 0){
12 /* 字元裝置注冊失敗, 自行處理 */
13 }
14 return 0;
15 }
16
17 /* 驅動出口函數 */
18 static void __exit xxx_exit(void)
19 {
20 /* 登出字元裝置驅動 */
21 unregister_chrdev(200, "chrtest");
22 }
23
24 /* 将上面兩個函數指定為驅動的入口和出口函數 */
25 module_init(xxx_init);
26 module_exit(xxx_exit);
第1行,定義了一個file_operations結構體變量test_fops,test_fops就是裝置的操作函數
集合,隻是此時我們還沒有初始化test_fops中的open、release等這些成員變
量,是以這個操作函數集合還是空的。
第10行,調用函數register_chrdev注冊字元裝置,主裝置号為200,裝置名字為
“chrtest”,裝置操作函數集合就是第1行定義的test_fops。
要注意的一點就是,選擇沒有被使用的主裝置号,輸入指令“cat /proc/devices”可以檢視
目前已經被使用掉的裝置号,如圖3所示(限于篇幅原因,隻展示一部分):
圖3 檢視目前裝置
在圖3中可以列出目前系統中所有的字元裝置和塊裝置,其中第1列就是裝置對應的主設
備号。200這個主裝置号在我的開發闆中并沒有被使用,是以我這裡就用了200這個主設
備号。
第21行,調用函數unregister_chrdev登出主裝置号為200的這個裝置。
3)實作裝置的具體操作函數
file_operations結構體就是裝置的具體操作函數,在之前我們定義了file_operations結構
體類型的變量test_fops,但是還沒對其進行初始化,也就是初始化其中的open、
release、read和write等具體的裝置操作函數。本節小節我們就完成變量test_fops的初始
化,設定好針對chrtest裝置的操作函數。在初始化test_fops之前我們要分析一下需求,
也就是要對chrtest這個裝置進行哪些操作,隻有确定了需求以後才知道我們應該實作哪
些操作函數。假設對chrtest這個裝置有如下兩個要求:
1、能夠對chrtest進行打開和關閉操作
裝置打開和關閉是最基本的要求,幾乎所有的裝置都得提供打開和關閉的功能。是以
我們需要實作file_operations中的open和release這兩個函數。
2、對chrtest進行讀寫操作
假設chrtest這個裝置控制着一段緩沖區(記憶體),應用程式需要通過read和write這兩個
函數對chrtest的緩沖區進行讀寫操作。是以需要實作file_operations中的read和write
這兩個函數。需求很清晰了,修改以上代碼,在其中加入test_fops這個結構體變量的
初始化操作,完成以後的内容如下所示:
1 /* 打開裝置 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 使用者實作具體功能 */
5 return 0;
6 }
7
8 /* 從裝置讀取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
10 {
11 /* 使用者實作具體功能 */
12 return 0;
13 }
14
15 /* 向裝置寫資料 */
16 static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
17 {
18 /* 使用者實作具體功能 */
19 return 0;
20 }
21
22 /* 關閉/ 釋放裝置 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 /* 使用者實作具體功能 */
26 return 0;
27 }
28
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
36
37 /* 驅動入口函數 */
38 static int __init xxx_init(void)
39 {
40 /* 入口函數具體内容 */
41 int retvalue = 0;
42
43 /* 注冊字元裝置驅動 */
44 retvalue = register_chrdev(200, "chrtest", &test_fops);
45 if(retvalue < 0){
46 /* 字元裝置注冊失敗,自行處理 */
47 }
48 return 0;
49 }
50
51 /* 驅動出口函數 */
52 static void __exit xxx_exit(void)
53 {
54 /* 登出字元裝置驅動 */
55 unregister_chrdev(200, "chrtest");
56 }
57
58 /* 将上面兩個函數指定為驅動的入口和出口函數 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
在以上代碼中我們一開始編寫了四個函數:chrtest_open、chrtest_read、
chrtest_write和chrtest_release。這四個函數就是chrtest裝置的open、read、write和
release操作函數。第29行~35行初始化test_fops的open、read、write和release這四
個成員變量。
4)添加LICENSE和作者資訊
最後我們需要在驅動中加入LICENSE資訊和作者資訊,其中LICENSE是必須添加的,否
則的話編譯的時候會報錯,作者資訊可以添加也可以不添加。LICENSE和作者資訊的添
加使用如下兩個函數:
MODULE_LICENSE() //添加子產品 LICENSE 資訊
MODULE_AUTHOR() //添加子產品作者資訊
最後給以上代碼加入LICENSE和作者資訊,完成以後的内容如下:
1 /* 打開裝置 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 使用者實作具體功能 */
5 return 0;
6 }
......
57
58 /* 将上面兩個函數指定為驅動的入口和出口函數 */
59 module_init(xxx_init);
60 module_exit(xxx_exit);
61
62 MODULE_LICENSE("GPL");
63 MODULE_AUTHOR("zuozhongkai");
第62行,LICENSE采用GPL協定。
第63行,添加作者名字。
至此,字元裝置驅動開發的完整步驟就講解完了,而且也編寫好了一個完整的字元裝置驅
動模闆,以後字元裝置驅動開發都可以在此模闆上進行。
3.Linux裝置号
1)裝置号的組成
為了友善管理,Linux中每個裝置都有一個裝置号,裝置号由主裝置号和次裝置号兩部分
組成,主裝置号表示某一個具體的驅動,次裝置号表示使用這個驅動的各個裝置。Linux
提供了一個名為dev_t的資料類型表示裝置号,dev_t定義在檔案include/linux/types.h裡
面,定義如下:
12 typedef __u32 __kernel_dev_t;
......
15 typedef __kernel_dev_t dev_t;
可以看出dev_t是__u32類型的,而__u32定義在檔案include/uapi/asm-generic/int-ll64.h
裡面,定義如下:
typedef unsigned int __u32;
綜上所述,dev_t其實就是unsigned int類型,是一個32位的資料類型。這32位的資料構
成了主裝置号和次裝置号兩部分,其中高12位為主裝置号,低20位為次裝置号。是以
Linux系統中主裝置号範圍為0~4095,是以大家在選擇主裝置号的時候一定不要超過這
個範圍。在檔案include/linux/kdev_t.h中提供了幾個關于裝置号的操作函數(本質是宏),
如下所示:
6 #define MINORBITS 20
7 #define MINORMASK ((1U << MINORBITS) - 1)
8
9 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
第6行,宏MINORBITS表示次裝置号位數,一共是20位。
第7行,宏MINORMASK表示次裝置号掩碼。
第9行,宏MAJOR用于從dev_t中擷取主裝置号,将dev_t右移20位即可。
第10行,宏MINOR用于從dev_t中擷取次裝置号,取dev_t的低20位的值即可。
第11行,宏MKDEV用于将給定的主裝置号和次裝置号的值組合成dev_t類型的裝置号。
2)裝置号的配置設定
1、靜态配置設定裝置号
本小節講的裝置号配置設定主要是主裝置号的配置設定。前面講解字元裝置驅動的時候說過
了,注冊字元裝置的時候需要給裝置指定一個裝置号,這個裝置号可以是驅動開發者
靜态的指定一個裝置号,比如選擇200這個主裝置号。有一些常用的裝置号已經被
Linux核心開發者給配置設定掉了,具體配置設定的内容可以檢視文檔
Documentation/devices.txt。并不是說核心開發者已經配置設定掉的主裝置号我們就不能
用了,具體能不能用還得看我們的硬體平台運作過程中有沒有使用這個主裝置号,使
用“cat /proc/devices”指令即可檢視目前系統中所有已經使用了的裝置号。
2、動态配置設定裝置号
靜态配置設定裝置号需要我們檢查目前系統中所有被使用了的裝置号,然後挑選一個沒
有使用的。而且靜态配置設定裝置号很容易帶來沖突問題,Linux社群推薦使用動态配置設定
裝置号,在注冊字元裝置之前先申請一個裝置号,系統會自動給你一個沒有被使用
的裝置号,這樣就避免了沖突。解除安裝驅動的時候釋放掉這個裝置号即可,裝置号的
申請函數如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函數alloc_chrdev_region用于申請裝置号,此函數有4個參數:
dev:儲存申請到的裝置号。
baseminor:次裝置号起始位址,alloc_chrdev_region可以申請一段連續的多個設
備号,這些裝置号的主裝置号一樣,但是次裝置号不同,次裝置号以
baseminor為起始位址位址開始遞增。一般baseminor為0,也就是說
次裝置号從0開始。
count:要申請的裝置号數量。
name:裝置名字。
登出字元裝置之後要釋放掉裝置号,裝置号釋放函數如下:
void unregister_chrdev_region(dev_t from, unsigned count)
此函數有兩個參數:
from:要釋放的裝置号。
count:表示從from開始,要釋放的裝置号數量。
4.chrdevbase字元裝置驅動開發實驗
字元裝置驅動開發的基本步驟我們已經了解了,本節我們就以chrdevbase這個虛拟裝置為例,
完整的編寫一個字元裝置驅動子產品。chrdevbase不是實際存在的一個裝置,是筆者為了友善講
解字元裝置的開發而引入的一個虛拟裝置。chrdevbase裝置有兩個緩沖區,一個為讀緩沖區,
一個為寫緩沖區,這兩個緩沖區的大小都為100位元組。在應用程式中可以向chrdevbase裝置的
寫緩沖區中寫入資料,從讀緩沖區中讀取資料。chrdevbase這個虛拟裝置的功能很簡單,但是
它包含了字元裝置的最基本功能。
1)實驗程式編寫
應用程式調用open函數打開chrdevbase這個裝置,打開以後可以使用write函數向
chrdevbase的寫緩沖區writebuf中寫入資料(不超過100個位元組),也可以使用read函數讀取
讀緩沖區readbuf中的資料操作,操作完成以後應用程式使用close函數關閉chrdevbase設
備。
建立chrdevbase.c,然後在裡面輸入如下内容:
1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
4 #include <linux/ide.h>
5 #include <linux/init.h>
6 #include <linux/module.h>
7 /***************************************************************
8 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
9 檔案名 : chrdevbase.c
10 作者 : 左忠凱
11 版本 : V1.0
12 描述 : chrdevbase 驅動檔案。
13 其他 : 無
14 論壇 : www.openedv.com
15 日志 : 初版 V1.0 2019/1/30 左忠凱建立
16 ***************************************************************/
17
18 #define CHRDEVBASE_MAJOR 200 /* 主裝置号 */
19 #define CHRDEVBASE_NAME "chrdevbase" /* 裝置名 */
20
21 static char readbuf[100]; /* 讀緩沖區 */
22 static char writebuf[100]; /* 寫緩沖區 */
23 static char kerneldata[] = {"kernel data!"};
24
25 /*
26 * @description : 打開裝置
27 * @param – inode : 傳遞給驅動的 inode
28 * @param - filp : 裝置檔案,file 結構體有個叫做 private_data 的成員變量
29 * 一般在 open 的時候将 private_data 指向裝置結構體。
30 * @return : 0 成功;其他 失敗
31 */
32 static int chrdevbase_open(struct inode *inode, struct file *filp)
33 {
34 //printk("chrdevbase open!\r\n");
35 return 0;
36 }
37
38 /*
39 * @description : 從裝置讀取資料
40 * @param - filp : 要打開的裝置檔案(檔案描述符)
41 * @param - buf : 傳回給使用者空間的資料緩沖區
42 * @param - cnt : 要讀取的資料長度
43 * @param - offt : 相對于檔案首位址的偏移
44 * @return : 讀取的位元組數,如果為負值,表示讀取失敗
45 */
46 static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
47 {
48 int retvalue = 0;
49
50 /* 向使用者空間發送資料 */
51 memcpy(readbuf, kerneldata, sizeof(kerneldata));
52 retvalue = copy_to_user(buf, readbuf, cnt);
53 if(retvalue == 0){
54 printk("kernel senddata ok!\r\n");
55 }else{
56 printk("kernel senddata failed!\r\n");
57 }
58
59 //printk("chrdevbase read!\r\n");
60 return 0;
61 }
62
63 /*
64 * @description : 向裝置寫資料
65 * @param - filp : 裝置檔案,表示打開的檔案描述符
66 * @param - buf : 要寫給裝置寫入的資料
67 * @param - cnt : 要寫入的資料長度
68 * @param - offt : 相對于檔案首位址的偏移
69 * @return : 寫入的位元組數,如果為負值,表示寫入失敗
70 */
71 static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
72 {
73 int retvalue = 0;
74 /* 接收使用者空間傳遞給核心的資料并且列印出來 */
75 retvalue = copy_from_user(writebuf, buf, cnt);
76 if(retvalue == 0){
77 printk("kernel recevdata:%s\r\n", writebuf);
78 }else{
79 printk("kernel recevdata failed!\r\n");
80 }
81
82 //printk("chrdevbase write!\r\n");
83 return 0;
84 }
85
86 /*
87 * @description : 關閉/釋放裝置
88 * @param - filp : 要關閉的裝置檔案(檔案描述符)
89 * @return : 0 成功;其他 失敗
90 */
91 static int chrdevbase_release(struct inode *inode, struct file *filp)
92 {
93 //printk("chrdevbase release!\r\n");
94 return 0;
95 }
96
97 /*
98 * 裝置操作函數結構體
99 */
100 static struct file_operations chrdevbase_fops = {
101 .owner = THIS_MODULE,
102 .open = chrdevbase_open,
103 .read = chrdevbase_read,
104 .write = chrdevbase_write,
105 .release = chrdevbase_release,
106 };
107
108 /*
109 * @description : 驅動入口函數
110 * @param : 無
111 * @return : 0 成功;其他 失敗
112 */
113 static int __init chrdevbase_init(void)
114 {
115 int retvalue = 0;
116
117 /* 注冊字元裝置驅動 */
118 retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
119 if(retvalue < 0){
120 printk("chrdevbase driver register failed\r\n");
121 }
122 printk("chrdevbase_init()\r\n");
123 return 0;
124 }
125
126 /*
127 * @description : 驅動出口函數
128 * @param : 無
129 * @return : 無
130 */
131 static void __exit chrdevbase_exit(void)
132 {
133 /* 登出字元裝置驅動 */
134 unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
135 printk("chrdevbase_exit()\r\n");
136 }
137
138 /*
139 * 将上面兩個函數指定為驅動的入口和出口函數
140 */
141 module_init(chrdevbase_init);
142 module_exit(chrdevbase_exit);
143
144 /*
145 * LICENSE 和作者資訊
146 */
147 MODULE_LICENSE("GPL");
148 MODULE_AUTHOR("zuozhongkai");
第32~36行,chrdevbase_open函數,當應用程式調用open函數的時候此函數就會調用,
本例程中我們沒有做任何工作,隻是輸出一串字元,用于調試。這裡使用了printk來輸出信
息,而不是printf!因為在Linux核心中沒有printf這個函數。printk相當于printf的孿生兄妹,
printf運作在使用者态,printk運作在核心态。在核心中想要向控制台輸出或顯示一些内容,
必須使用printk這個函數。不同之處在于,printk可以根據日志級别對消息進行分類,一共
有8個消息級别,這8個消息級别定義在檔案include/linux/kern_levels.h裡面,定義如下:
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 緊急事件,一般是核心崩潰 */
#define KERN_ALERT KERN_SOH "1" /* 必須立即采取行動 */
#define KERN_CRIT KERN_SOH "2" /* 臨界條件,比如嚴重的軟體或硬體錯誤*/
#define KERN_ERR KERN_SOH "3" /* 錯誤狀态,一般裝置驅動程式中使用KERN_ERR 報告硬體錯誤 */
#define KERN_WARNING KERN_SOH "4" /* 警告資訊,不會對系統造成嚴重影響 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要進行提示的一些資訊 */
#define KERN_INFO KERN_SOH "6" /* 提示性的資訊 */
#define KERN_DEBUG KERN_SOH "7" /* 調試資訊 */
一共定義了8個級别,其中0的優先級最高,7的優先級最低。如果要設定消息級别,參考
如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
上述代碼就是設定“gsmi:LogShutdownReason\n”這行消息的級别為KERN_EMERG。在
具體的消息前面加上KERN_EMERG就可以将這條消息的級别設定為KERN_EMERG。如
果使用printk的時候不顯式的設定消息級别,那麼printk将會采用預設級别
MESSAGE_LOGLEVEL_DEFAULT,MESSAGE_LOGLEVEL_DEFAULT預設為4。在
include/linux/printk.h中有個宏CONSOLE_LOGLEVEL_DEFAULT,定義如下:
#define CONSOLE_LOGLEVEL_DEFAULT 7
CONSOLE_LOGLEVEL_DEFAULT控制着哪些級别的消息可以顯示在控制台上,此宏默
認為7,意味着隻有優先級高于7的消息才能顯示在控制台上。這個就是printk和printf的最
大差別,可以通過消息級别來決定哪些消息可以顯示在控制台上。預設消息級别為4,4的
級别比7高,所示直接使用printk輸出的資訊是可以顯示在控制台上的。
參數filp有個叫做private_data的成員變量,private_data是個void指針,一般在驅動中将
private_data指向裝置結構體,裝置結構體會存放裝置的一些屬性。
第46~61行,chrdevbase_read函數,應用程式調用read函數從裝置中讀取資料的時候此
函數會執行。參數buf是使用者空間的記憶體,讀取到的資料存儲在buf中,參數cnt是要讀取的
位元組數,參數offt是相對于檔案首位址的偏移。kerneldata裡面儲存着使用者空間要讀取的數
據,第51行先将kerneldata數組中的資料拷貝到讀緩沖區readbuf中,第52行通過函數
copy_to_user将readbuf中的資料複制到參數buf中。因為核心空間不能直接操作使用者空間的
記憶體,是以需要借助copy_to_user函數來完成核心空間的資料到使用者空間的複制。
copy_to_user函數原型如下:
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
參數to表示目的,參數from表示源,參數n表示要複制的資料長度。如果複制成功,傳回值
為0,如果複制失敗則傳回負數。
第71~84行,chrdevbase_write函數,應用程式調用write函數向裝置寫資料的時候此函數
就會執行。參數buf就是應用程式要寫入裝置的資料,也是使用者空間的記憶體,參數cnt是要
寫入的資料長度,參數offt是相對檔案首位址的偏移。第75行通過函數copy_from_user将
buf中的資料複制到寫緩沖區writebuf中,因為使用者空間記憶體不能直接通路核心空間的内
存,是以需要借助函數copy_from_user将使用者空間的資料複制到writebuf這個核心空間
中。
第91~95行,chrdevbase_release函數,應用程式調用close關閉裝置檔案的時候此函數會
執行,一般會在此函數裡面執行一些釋放操作。如果在open函數中設定了filp的
private_data成員變量指向裝置結構體,那麼在release函數最終就要釋放掉。
第100~106行,建立chrdevbase的裝置檔案操作結構體chrdevbase_fops,初始化
chrdevbase_fops。
第113~124行,驅動入口函數chrdevbase_init,第118行調用函數register_chrdev來注冊字
符裝置。
第131~136行,驅動出口函數chrdevbase_exit,第134行調用函數unregister_chrdev來注
銷字元裝置。
第141~142行,通過module_init和module_exit這兩個函數來指定驅動的入口和出口函
數。
第147~148行,添加LICENSE和作者資訊。
2)編寫測試APP
1、C庫檔案操作基本函數
編寫測試APP就是編寫Linux應用,需要用到C庫裡面和檔案操作有關的一些函數,比如
open、read、write和close這四個函數。
1.open函數open函數原型如下:
int open(const char *pathname, int flags)
open函數參數含義如下:
pathname:要打開的裝置或者檔案名。
flags:檔案打開模式,以下三種模式必選其一:
O_RDONLY 隻讀模式
O_WRONLY 隻寫模式
O_RDWR 讀寫模式
因為我們要對chrdevbase這個裝置進行讀寫操作,是以選擇O_RDWR。
除了上述三種模式以外還有其他的可選模式,通過邏輯或來選擇多種模式:
O_APPEND 每次寫操作都寫入檔案的末尾
O_CREAT 如果指定檔案不存在,則建立這個檔案
O_EXCL 如果要建立的檔案已存在,則傳回-1,并且修改errno的值
O_TRUNC 如果檔案存在,并且以隻寫/讀寫方式打開,則清空檔案全
部内容
O_NOCTTY 如果路徑名指向終端裝置,不要把這個裝置用作控制終
端。
O_NONBLOCK 如果路徑名指向FIFO/塊檔案/字元檔案,則把檔案的打開
和後繼I/O設定為非阻塞DSYNC等待實體I/O結束後再
write。在不影響讀取新寫入的資料的前提下,不等待文
件屬性更新。
O_RSYNCread 等待所有寫入同一區域的寫操作完成後再進行。
O_SYNC 等待實體I/O結束後再write,包括更新檔案屬性的I/O。
傳回值:如果檔案打開成功的話傳回檔案的檔案描述符。
在Ubuntu中輸入“man 2 open”即可檢視open函數的詳細内容,如圖4所示:
圖4 open函數幫助資訊
2.read函數
read函數原型如下:
ssize_t read(int fd, void *buf, size_t count)
read函數參數含義如下:
fd:要讀取的檔案描述符,讀取檔案之前要先用open函數打開檔案,open函數打開
檔案成功以後會得到檔案描述符。
buf:資料讀取到此buf中。
count:要讀取的資料長度,也就是位元組數。
傳回值:讀取成功的話傳回讀取到的位元組數;如果傳回0表示讀取到了檔案末尾;如
果傳回負值,表示讀取失敗。
在Ubuntu中輸入“man 2 read”指令即可檢視read函數的詳細内容。
3.write函數
write函數原型如下:
ssize_t write(int fd, const void *buf, size_t count);
write函數參數含義如下:
fd:要進行寫操作的檔案描述符,寫檔案之前要先用open函數打開檔案,open函數
打開檔案成功以後會得到檔案描述符。
buf:要寫入的資料。
count:要寫入的資料長度,也就是位元組數。
傳回值:寫入成功的話傳回寫入的位元組數;如果傳回0表示沒有寫入任何資料;如果
傳回負值,表示寫入失敗。
在Ubuntu中輸入“man 2 write”指令即可檢視write函數的詳細内容。
4.close函數
close函數原型如下:
int close(int fd);
close函數參數含義如下:
fd:要關閉的檔案描述符。
傳回值:0表示關閉成功,負值表示關閉失敗。
在Ubuntu中輸入“man 2 close”指令即可檢視close函數的詳細内容。
2、編寫測試APP程式
驅動編寫好以後是需要測試的,一般編寫一個簡單的測試APP,測試APP運作在用
戶空間。測試APP很簡單通過輸入相應的指令來對chrdevbase裝置執行讀或者寫操
作。在1_chrdevbase目錄中建立chrdevbaseApp.c檔案,在此檔案中輸入如下内
容:
1 #include "stdio.h"
2 #include "unistd.h"
3 #include "sys/types.h"
4 #include "sys/stat.h"
5 #include "fcntl.h"
6 #include "stdlib.h"
7 #include "string.h"
8 /***************************************************************
9 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
10 檔案名 : chrdevbaseApp.c
11 作者 : 左忠凱
12 版本 : V1.0
13 描述 : chrdevbase 驅測試 APP。
14 其他 : 使用方法:./chrdevbaseApp /dev/chrdevbase <1>|<2>
15 argv[2] 1:讀檔案
16 argv[2] 2:寫檔案
17 論壇 : www.openedv.com
18 日志 : 初版 V1.0 2019/1/30 左忠凱建立
19 ***************************************************************/
20
21 static char usrdata[] = {"usr data!"};
22
23 /*
24 * @description : main 主程式
25 * @param - argc : argv 數組元素個數
26 * @param - argv : 具體參數
27 * @return : 0 成功;其他 失敗
28 */
29 int main(int argc, char *argv[])
30 {
31 int fd, retvalue;
32 char *filename;
33 char readbuf[100], writebuf[100];
34
35 if(argc != 3){
36 printf("Error Usage!\r\n");
37 return -1;
38 }
39
40 filename = argv[1];
41
42 /* 打開驅動檔案 */
43 fd = open(filename, O_RDWR);
44 if(fd < 0){
45 printf("Can't open file %s\r\n", filename);
46 return -1;
47 }
48
49 if(atoi(argv[2]) == 1){ /* 從驅動檔案讀取資料 */
50 retvalue = read(fd, readbuf, 50);
51 if(retvalue < 0){
52 printf("read file %s failed!\r\n", filename);
53 }else{
54 /* 讀取成功,列印出讀取成功的資料 */
55 printf("read data:%s\r\n",readbuf);
56 }
57 }
58
59 if(atoi(argv[2]) == 2){
60 /* 向裝置驅動寫資料 */
61 memcpy(writebuf, usrdata, sizeof(usrdata));
62 retvalue = write(fd, writebuf, 50);
63 if(retvalue < 0){
64 printf("write file %s failed!\r\n", filename);
65 }
66 }
67
68 /* 關閉裝置 */
69 retvalue = close(fd);
70 if(retvalue < 0){
71 printf("Can't close file %s\r\n", filename);
72 return -1;
73 }
74
75 return 0;
76 }
第21行,數組usrdata是測試APP要向chrdevbase裝置寫入的資料。
第35行,判斷運作測試APP的時候輸入的參數是不是為3個,main函數的argc參數表
示參數數量,argv[]儲存着具體的參數,如果參數不為3個的話就表示測試
APP用法錯誤。比如,現在要從chrdevbase裝置中讀取資料,需要輸入如
下指令:
./chrdevbaseApp /dev/chrdevbase 1
上述指令一共有三個參數“./chrdevbaseApp”、“/dev/chrdevbase”和“1”,這三個參數
分别對應argv[0]、argv[1]和argv[2]。第一個參數表示運作chrdevbaseAPP這個軟
件,第二個參數表示測試APP要打開/dev/chrdevbase這個裝置。第三個參數就是要
執行的操作,1表示從chrdevbase中讀取資料,2表示向chrdevbase寫資料。
第40行,擷取要打開的裝置檔案名字,argv[1]儲存着裝置名字。
第43行,調用C庫中的open函數打開裝置檔案:/dev/chrdevbase。
第49行,判斷argv[2]參數的值是1還是2,因為輸入指令的時候其參數都是字元串格
式的,是以需要借助atoi函數将字元串格式的數字轉換為真實的數字。
第50行,當argv[2]為1的時候表示要從chrdevbase裝置中讀取資料,一共讀取50字
節的資料,讀取到的資料儲存在readbuf中,讀取成功以後就在終端上打
印出讀取到的資料。
第59行,當argv[2]為2的時候表示要向chrdevbase裝置寫資料。
第69行,對chrdevbase裝置操作完成以後就關閉裝置。
chrdevbaseApp.c内容還是很簡單的,就是最普通的檔案打開、關閉和讀寫操作。
3)編譯驅動程式和測試APP
1、編譯驅動程式
首先編譯驅動程式,也就是chrdevbase.c這個檔案,我們需要将其編譯為.ko子產品,創
建Makefile檔案,然後在其中輸入如下内容:
1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-
rel_imx_4.1.15_2.1.0_ga_alientek
2 CURRENT_PATH := $(shell pwd)
3 obj-m := chrdevbase.o
4
5 build: kernel_modules
6
7 kernel_modules:
8 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
9 clean:
10 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第1行,KERNELDIR表示開發闆所使用的Linux核心源碼目錄,使用絕對路徑,大家
根據自己的實際情況填寫即可。
第2行,CURRENT_PATH表示目前路徑,直接通過運作“pwd”指令來擷取目前所處路
徑。
第3行,obj -m表示将chrdevbase.c這個檔案編譯為chrdevbase.ko子產品。
第8行,具體的編譯指令,後面的modules表示編譯子產品,-C表示将目前的工作目錄
切換到指定目錄中,也就是KERNERLDIR目錄。M表示子產品源碼目錄,
“make modules”指令中加入M=dir以後程式會自動到指定的dir目錄中讀取模
塊的源碼并将其編譯為.ko檔案。
Makefile編寫好以後輸入“make”指令編譯驅動子產品,編譯過程如圖5所示:
圖5 驅動子產品編譯過程
編譯成功以後就會生成一個叫做chrdevbaes.ko的檔案,此檔案就是chrdevbase設
備的驅動子產品。至此,chrdevbase裝置的驅動就編譯成功。
2、編譯測試APP
測試APP比較簡單,隻有一個檔案,是以就不需要編寫Makefile了,直接輸入指令編
譯。因為測試APP是要在ARM開發闆上運作的,是以需要使用arm-linux-gnueabihf-
gcc來編譯,輸入如下指令:
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
編譯完成以後會生成一個叫做chrdevbaseApp的可執行程式,輸入如下指令檢視
chrdevbaseAPP這個程式的檔案資訊:
file chrdevbaseApp
結果如圖6所示:
圖6 chrdevbaseAPP檔案資訊
從圖6可以看出,chrdevbaseAPP這個可執行檔案是32位LSB格式,ARM版本的,
是以chrdevbaseAPP隻能在ARM晶片下運作。
4)運作測試
1、加載驅動子產品
驅動子產品chrdevbase.ko和測試軟體chrdevbaseAPP都已經準備好了,接下來就是運
行測試。為了友善測試,Linux系統選擇通過TFTP從網絡啟動,并且使用NFS挂載
網絡根檔案系統,確定uboot中bootcmd環境變量的值為:
tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000
bootrags環境變量的值為:
console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250:/home/zuozhongkai/linux/nfs/
rootfs ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off
設定好以後啟動Linux系統,檢查開發闆根檔案系統中有沒有“/lib/modules/4.1.15”這
個目錄,如果沒有的話自行建立。因為是通過NFS将Ubuntu中的rootfs目錄挂載為根
檔案系統,是以可以很友善的将chrdevbase.ko和chrdevbaseAPP複制到
rootfs/lib/modules/4.1.15目錄中,指令如下:
sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/ -f
拷貝完成以後就會在開發闆的/lib/modules/4.1.15目錄下存在chrdevbase.ko和
chrdevbaseAPP這兩個檔案,如圖7所示:
圖7 驅動和測試檔案
輸入如下指令加載chrdevbase.ko驅動檔案:
insmod chrdevbase.ko
或者:
modprobe chrdevbase.ko
如果使用modprobe加載驅動的話,可能會出現如圖8所示的提示:
圖8 modprobe錯誤提示
從圖8可以看出,modprobe提示無法打開“modules.dep”這個檔案,是以驅動挂載失
敗了。我們不用手動建立modules.dep這個檔案,直接輸入depmod指令即可自動生
成modules.dep,有些根檔案系統可能沒有depmod這個指令,如果沒有這個指令就
隻能重新配置busybox,使能此指令,然後重新編譯busybox。
輸入“depmod”指令以後會自動生成modules.alias、modules.symbols和
modules.dep這三個檔案,如圖9所示:
圖9 depmod指令執行結果
重新使用modprobe加載chrdevbase.ko,結果如圖10所示:
圖10 驅動加載成功
從圖10可以看到“chrdevbaseinit!”這一行,這一行正是chrdevbase.c中子產品入口
函數chrdevbase_init輸出的資訊,說明子產品加載成功!輸入“lsmod”指令即可檢視
目前系統中存在的子產品,結果如圖11所示:
圖11 目前系統中的子產品
從圖11可以看出,目前系統隻有“chrdevbase”這一個子產品。輸入如下指令檢視當
前系統中有沒有chrdevbase這個裝置:
圖12 目前系統裝置
從圖12可以看出,目前系統存在chrdevbase這個裝置,主裝置号為200,跟我
們設定的主裝置号一緻。
2、建立裝置節點檔案
驅動加載成功需要在/dev目錄下建立一個與之對應的裝置節點檔案,應用程式就是通
過操作這個裝置節點檔案來完成對具體裝置的操作。輸入如下指令創
建/dev/chrdevbase這個裝置節點檔案:
mknod /dev/chrdevbase c 200 0
其中“mknod”是建立節點指令,“/dev/chrdevbase”是要建立的節點檔案,“c”表示這是
個字元裝置,“200”是裝置的主裝置号,“0”是裝置的次裝置号。建立完成以後就會存
在/dev/chrdevbase這個檔案,可以使用“ls /dev/chrdevbase -l”指令檢視,結果如圖
13所示:
圖13 /dev/chrdevbase檔案
如果chrdevbaseAPP想要讀寫chrdevbase裝置,直接對/dev/chrdevbase進行讀寫操
作即可。相當于/dev/chrdevbase這個檔案是chrdevbase裝置在使用者空間中的實作。
前面一直說Linux下一切皆檔案,包括裝置也是檔案,現在大家應該是有這個概念了
吧?
3、chrdevbase裝置操作測試
一切準備就緒,接下來就是“大考”的時刻了。使用chrdevbaseApp軟體操作
chrdevbase這個裝置,看看讀寫是否正常,首先進行讀操作,輸入如下指令:
./chrdevbaseApp /dev/chrdevbase 1
結果如圖14所示:
圖14 讀操作結果
從圖14可以看出,首先輸出“kernel send data ok!”這一行資訊,這是驅動程式中
chrdevbase_read函數輸出的資訊,因為chrdevbaseAPP使用read函數從
chrdevbase裝置讀取資料,是以chrdevbase_read函數就會執行。chrdevbase_read
函數向chrdevbaseAPP發送“kernel data!”資料,chrdevbaseAPP接收到以後就列印
出來,“read data:kernel data!”就是chrdevbaseAPP列印出來的接收到的資料。說明
對chrdevbase的讀操作正常,接下來測試對chrdevbase裝置的寫操作,輸入如下命
令:
./chrdevbaseApp /dev/chrdevbase 2
結果如圖15所示:
圖15 寫操作結果
隻有一行“kernel recevdata:usrdata!”,這個是驅動程式中的chrdevbase_write函數輸
出的。chrdevbaseAPP使用write函數向chrdevbase裝置寫入資料“usr data!”。
chrdevbase_write函數接收到以後将其列印出來。說明對chrdevbase的寫操作正常,
既然讀寫都沒問題,說明我們編寫的chrdevbase驅動是沒有問題的。
4、解除安裝驅動子產品
如果不再使用某個裝置的話可以将其驅動解除安裝掉,比如輸入如下指令解除安裝掉
chrdevbase這個裝置:
rmmod chrdevbase.ko
解除安裝以後使用lsmod指令檢視chrdevbase這個子產品還存不存在,結果如圖16所示:
圖16 系統中目前子產品
從圖16可以看出,此時系統已經沒有任何子產品了,chrdevbase這個子產品也不存在
了,說明子產品解除安裝成功。至此,chrdevbase這個裝置的整個驅動就驗證完成了,驅
動工作正常。
本章我們詳細的講解了字元裝置驅動的開發步驟,并且以一個虛拟的chrdevbase設
備為例,帶領大家完成了第一個字元裝置驅動的開發,掌握了字元裝置驅動的開發
架構以及測試方法,以後的字元裝置驅動實驗基本都以此為藍本。