虚拟字符设备驱动开发步骤
目錄
- 前言
- 字符設備驅動簡介
- 內核驅動操作函數集合(file_operations結構體)
- 字符設備驅動開發步驟
- .ko驅動模塊的加載和卸載(module_init驅動入口、insmod驅動加載)
- 字符設備注冊與注銷到內核register_chrdev(設備號、設備名) -- 很少用了
- 實現設備的具體操作函數
- 添加LICENSE 和作者信息
- 創建應用層設備節點文件(mknod應用程序通過設備節點操作具體設備)
- Linux 設備號
- 設備號的組成(主、次設備號)
- 設備號的分配
- chrdevbase字符設備驅動實驗程序編寫
- 1、創建VSCode 工程
- 2、VSCode添加頭文件路徑(重要)
- 3、編寫驅動程序(傳遞函數copy_to_user、copy_from_user)
- 編寫測試APP(1表示向驅動讀數據、2表示向驅動寫數據)
- Makefile編譯驅動程序和測試APP
- 1、編譯驅動程序
- 2、編譯測試APP
- 運行測試
- 1、設置內核啟動、復制.ko和app到指定目錄、加載驅動模塊
- 2、創建設備節點文件(mknod應用程序通過設備節點操作具體設備)
- 3、chrdevbase設備操作測試
- 4、卸載驅動模塊
前言
【注意】驅動開發不要用正點原子提供的uboot和linux內核以及根文件系統,因為里面所有驅動都寫好了,我們要使用恩智浦提供的。
Linux 中的三大類驅動:字符設備驅動、塊設備驅動和網絡設備驅動。
字符設備驅動是占用篇幅最大的一類驅動,因為字符設備最多,從最簡單的點燈到I2C、SPI、音頻等都屬于字符設備驅動的類型。
塊設備和網絡設備驅動要比字符設備驅動復雜,就是因為其復雜所以半導體廠商一般都給我們編寫好了,大多數情況下都是直接可以使用的。
所謂的塊設備驅動就是存儲器設備的驅動,比如EMMC、NAND、SD 卡和U 盤等存儲設備,因為這些存儲設備的特點是以存儲塊為基礎,因此叫做塊設備。
網絡設備驅動就更好理解了,就是網絡驅動,不管是有線的還是無線的,都屬于網絡設備驅動的范疇。
一個設備可以屬于多種設備驅動類型,比如USB WIFI,其使用USB 接口,所以屬于字符設備,但是其又能上網,所以也屬于網絡設備驅動。
本書使用的Linux 內核版本為4.1.15,其支持設備樹(Device tree),所以本篇所有例程均采用設備樹。
本章會以一個虛擬的設備為例,講解如何進行字符設備驅動開發,以及如何編寫測試APP 來測試驅動工作是否正常,為以后的學習打下堅實的基礎。
字符設備驅動簡介
字符設備是Linux 驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先后順序的。比如我們最常見的點燈、按鍵、IIC、SPI,LCD 等等都是字符設備。
我們先來簡單的了解一下Linux 下的應用程序是如何調用驅動程序的,Linux 應用程序對驅動程序的調用如圖40.1.1 所示:
在Linux 中一切皆為文件,驅動加載成功以后會在“/dev”目錄下生成一個相應的文件,應用程序通過對這個名為“/dev/xxx”(xxx 是具體的驅動文件名字)的文件進行相應的操作即可實現對硬件的操作。應用程序使用open 函數來打開文件/dev/led,使用完成以后使用close 函數關閉/dev/led 這個文件。open
和close 就是打開和關閉led 驅動的函數,如果要獲取led 燈的狀態,就用read 函數從驅動中讀取相應的狀態。
應用程序運行在用戶空間,而Linux 驅動屬于內核的一部分,因此驅動運行于內核空間。當我們在用戶空間想要實現對內核的操作,比如使用open 函數打開/dev/led 這個驅動,因為用戶空間不能直接對內核進行操作,因此必須使用一個叫做“系統調用”的方法來實現從用戶空間“陷入”到內核空間,這樣才能實現對底層驅動的操作。open、close、write 和read 等這些函數是由C 庫提供的,在Linux 系統中,系統調用作為C 庫的一部分。當我們調用open 函數的時候流程如圖40.1.2 所示:
其中關于C 庫以及如何通過系統調用“陷入”到內核空間這個我們不用去管,我們重點關注的是應用程序和具體的驅動,應用程序使用到的函數在具體驅動程序中都有與之對應的函數,比如應用程序中調用了open 這個函數,那么在驅動程序中也得有一個名為open 的函數。每一個系統調用,在驅動中都有與之對應的一個驅動函數,在Linux內核文件include/linux/fs.h 中有個叫做file_operations 的結構體,此結構體就是Linux 內核驅動操作函數集合,內容如下所示:
內核驅動操作函數集合(file_operations結構體)
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 等都是需要實現的。
字符設備驅動開發步驟
.ko驅動模塊的加載和卸載(module_init驅動入口、insmod驅動加載)
Linux 驅動有兩種運行方式
- 【編譯進內核】第一種就是將驅動編譯進Linux 內核中,這樣當Linux 內核啟動的時候就會自動運行驅動程序
- 【編譯成.ko模塊】第二種就是將驅動編譯成模塊(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.koinsmod 命令不能解決模塊的依賴關系,比如drv.ko 依賴first.ko 這個模塊,就必須先使用insmod 命令加載first.ko 這個模塊,然后再加載drv.ko 這個模塊。但是modprobe 就不會存在這個問題,modprobe 會分析模塊的依賴關系,然后會將所有的依賴模塊都加載到內核中,因此modprobe 命令相比insmod 要智能一些。modprobe 命令主要智能在提供了模塊的依賴性分析、錯誤檢查、錯誤報告等功能,推薦使用modprobe 命令來加載驅動。modprobe 命令默認會去/lib/modules/< kernel-version>目錄中查找模塊,比如本書使用的Linux kernel 的版本號為4.1.15,因此modprobe 命令默認會到/lib/modules/4.1.15 這個目錄中查找相應的驅動模塊,一般自己制作的根文件系統中是不會有這個目錄的,所以需要自己手動創建,然后將.ko文件放到這個文件目錄即可。
加載成功以后可以使用lsmod命令查看一下。
驅動模塊的卸載使用命令“rmmod”即可,比如要卸載drv.ko,使用如下命令即可:
rmmod drv.ko也可以使用“modprobe -r”命令卸載驅動,比如要卸載drv.ko,命令如下:
modprobe -r drv.ko使用modprobe 命令可以卸載掉驅動模塊所依賴的其他模塊,前提是這些依賴模塊已經沒有被其他模塊所使用,否則就不能使用modprobe 來卸載驅動模塊。所以對于模塊的卸載,還是推薦使用rmmod 命令。
字符設備注冊與注銷到內核register_chrdev(設備號、設備名) – 很少用了
這個函數現在已經很少用了,后面會講到有新的函數代替。它有兩個缺點:
- 把次設備號全占用了
- 需要手動查看哪些設備號沒占用,不能自動申請
對于字符設備驅動而言,當驅動模塊加載成功以后需要注冊字符設備,同樣,卸載驅動模塊的時候也需要注銷掉字符設備。字符設備的注冊和注銷函數原型如下所示:
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 中進行。在示例代碼40.2.2.1 中字符設備的注冊和注銷,內容如下所示:
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可以查看當前已經被使用掉的設備號,如圖40.2.2.1 所示(限于篇幅原因,只展示一部分):
在圖40.2.2.1 中可以列出當前系統中所有的字符設備和塊設備,其中第1 列就是設備對應的主設備號。200 這個主設備號在我的開發板中并沒有被使用,所以我這里就用了200 這個主設備號。
第21 行,調用函數unregister_chrdev 注銷主設備號為200 的這個設備。
實現設備的具體操作函數
file_operations 結構體就是設備的具體操作函數,在示例代碼40.2.2.1 中我們定義了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 這兩個函數。
需求很清晰了,修改示例代碼40.2.2.1,在其中加入test_fops 這個結構體變量的初始化操作,完成以后的內容如下所示:
在示例代碼40.2.3.1 中我們一開始編寫了四個函數:chrtest_open、chrtest_read、chrtest_write和chrtest_release。這四個函數就是chrtest 設備的open、read、write 和release 操作函數。第29行~35 行初始化test_fops 的open、read、write 和release 這四個成員變量。
添加LICENSE 和作者信息
最后我們需要在驅動中加入LICENSE 信息和作者信息,其中LICENSE 是必須添加的,否則的話編譯的時候會報錯,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用如下兩個函數:
MODULE_LICENSE() //添加模塊LICENSE 信息 MODULE_AUTHOR() //添加模塊作者信息最后給示例代碼40.2.3.1 加入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 行,添加作者名字。
創建應用層設備節點文件(mknod應用程序通過設備節點操作具體設備)
驅動加載成功需要在/dev 目錄下創建一個與之對應的設備節點文件,應用程序就是通過操作這個設備節點文件來完成對具體設備的操作。輸入如下命令創建/dev/chrdevbase 這個設備節點文件:
mknod /dev/chrdevbase c 200 0其中:
- “mknod”是創建節點命令
- “/dev/chrdevbase”是要創建的節點文件
- “c”表示這是個字符設備
- “200”是設備的主設備號
- “0”是設備的次設備號。
創建完成以后就會存在/dev/chrdevbase 這個文件,可以使用“ls /dev/chrdevbase -l”命令查看,結果如圖40.4.4.7 所示:
如果chrdevbaseAPP 想要讀寫chrdevbase 設備,直接對/dev/chrdevbase 進行讀寫操作即可,相當于/dev/chrdevbase 這個文件是chrdevbase 設備在用戶空間中的實現。Linux 下一切皆文件!
至此,字符設備驅動開發的完整步驟就講解完了,而且也編寫好了一個完整的字符設備驅動模板,以后字符設備驅動開發都可以在此模板上進行。
Linux 設備號
設備號的組成(主、次設備號)
為了方便管理,Linux 中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成:
- 主設備號表示某一個具體的驅動(如IIC)
- 次設備號表示使用這個驅動的各個設備(如好多個IIC設備)。
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 里面,定義如下:
26 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 類型的設備號。
設備號的分配
1、靜態分配設備號
本小節講的設備號分配主要是主設備號的分配。前面講解字符設備驅動的時候說過了,注冊字符設備的時候需要給設備指定一個設備號,這個設備號可以是驅動開發者靜態的指定一個設備號,比如選擇200 這個主設備號。有一些常用的設備號已經被Linux 內核開發者給分配掉了,具體分配的內容可以查看文檔Documentation/devices.txt。并不是說內核開發者已經分配掉的主設備號我們就不能用了,具體能不能用還得看我們的硬件平臺運行過程中有沒有使用這個主設備號,使用“cat /proc/devices”命令即可查看當前系統中所有已經使用了的設備號。
2、動態分配設備號
靜態分配設備號需要我們檢查當前系統中所有被使用了的設備號,然后挑選一個沒有使用的。而且靜態分配設備號很容易帶來沖突問題,Linux 社區推薦使用動態分配設備號,在注冊字符設備之前先申請一個設備號,系統會自動給你一個沒有被使用的設備號,這樣就避免了沖突。
卸載驅動的時候釋放掉這個設備號即可,設備號的申請函數如下:
函數alloc_chrdev_region 用于申請設備號,此函數有4 個參數:
dev:保存申請到的設備號。
baseminor:次設備號起始地址,alloc_chrdev_region 可以申請一段連續的多個設備號,這些設備號的主設備號一樣,但是次設備號不同,次設備號以baseminor 為起始地址地址開始遞增。一般baseminor 為0,也就是說次設備號從0 開始。
count:要申請的設備號數量。
name:設備名字。
注銷字符設備之后要釋放掉設備號,設備號釋放函數如下:
此函數有兩個參數:
from:要釋放的設備號。
count:表示從from 開始,要釋放的設備號數量。
chrdevbase字符設備驅動實驗程序編寫
字符設備驅動開發的基本步驟我們已經了解了,本節我們就以chrdevbase 這個虛擬設備為例,完整的編寫一個字符設備驅動模塊。chrdevbase 不是實際存在的一個設備,是筆者為了方便講解字符設備的開發而引入的一個虛擬設備。chrdevbase 設備有兩個緩沖區,一個為讀緩沖區,一個為寫緩沖區,這兩個緩沖區的大小都為100 字節。在應用程序中可以向chrdevbase 設備的寫緩沖區中寫入數據,從讀緩沖區中讀取數據。chrdevbase 這個虛擬設備的功能很簡單,但是它包含了字符設備的最基本功能。
本實驗對應的例程路徑為:開發板光盤-> 2、Linux 驅動例程-> 1_chrdevbase。
應用程序調用open 函數打開chrdevbase 這個設備,打開以后可以使用write 函數向chrdevbase 的寫緩沖區writebuf 中寫入數據(不超過100 個字節),也可以使用read 函數讀取讀緩沖區readbuf 中的數據操作,操作完成以后應用程序使用close 函數關閉chrdevbase 設備。
1、創建VSCode 工程
在Ubuntu 中創建一個目錄用來存放Linux 驅動程序,比如我創建了一個名為Linux_Drivers的目錄來存放所有的Linux 驅動。在Linux_Drivers 目錄下新建一個名為1_chrdevbase 的子目錄來存放本實驗所有文件,如圖40.4.1.1 所示:
在1_chrdevbase 目錄中新建VSCode 工程,并且新建chrdevbase.c 文件,完成以后1_chrdevbase 目錄中的文件如圖40.4.1.2 所示:
2、VSCode添加頭文件路徑(重要)
因為是編寫Linux 驅動,因此會用到Linux 源碼中的函數。我們需要在VSCode 中添加Linux源碼中的頭文件路徑。打開VSCode,按下“Crtl+Shift+P”打開VSCode 的控制臺,然后輸入“C/C++: Edit configurations(JSON) ”,打開C/C++編輯配置文件,如圖40.4.1.3 所示:
打開以后會自動在.vscode 目錄下生成一個名為c_cpp_properties.json 的文件,此文件默認內容如下所示:
第5 行的includePath 表示頭文件路徑,需要將Linux 源碼里面的頭文件路徑添加進來,也就是我們前面移植的Linux 源碼中的頭文件路徑。添加頭文件路徑以后的c_cpp_properties.json的文件內容如下所示:
1 { 2 "configurations": [ 3 { 4 "name": "Linux", 5 "includePath": [ 6 "${workspaceFolder}/**", 7 "/home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include", 8 "/home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include", 9 "/home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/" 10 ], 11 "defines": [], ...... 16 } 17 ], 18 "version": 4 19 }第7~9 行就是添加好的Linux 頭文件路徑。分別是開發板所使用的Linux 源碼下的include、arch/arm/include 和arch/arm/include/generated 這三個目錄的路徑,注意,這里使用了絕對路徑。
3、編寫驅動程序(傳遞函數copy_to_user、copy_from_user)
工程建立好以后就可以開始編寫驅動程序了,新建chrdevbase.c,然后在里面輸入如下內容:
#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> /*************************************************************** Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : chrdevbase.c 作者 : 左忠凱 版本 : V1.0 描述 : chrdevbase驅動文件。 其他 : 無 論壇 : www.openedv.com 日志 : 初版V1.0 2019/1/30 左忠凱創建 ***************************************************************/#define CHRDEVBASE_MAJOR 200 /* 主設備號 */ #define CHRDEVBASE_NAME "chrdevbase" /* 設備名 */static char readbuf[100]; /* 讀緩沖區 */ static char writebuf[100]; /* 寫緩沖區 */ static char kerneldata[] = {"kernel data!"};/** @description : 打開設備* @param - inode : 傳遞給驅動的inode* @param - filp : 設備文件,file結構體有個叫做private_data的成員變量* 一般在open的時候將private_data指向設備結構體。* @return : 0 成功;其他 失敗*/ static int chrdevbase_open(struct inode *inode, struct file *filp) {//printk("chrdevbase open!\r\n");return 0; }/** @description : 從設備讀取數據 * @param - filp : 要打開的設備文件(文件描述符)* @param - buf : 返回給用戶空間的數據緩沖區* @param - cnt : 要讀取的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 讀取的字節數,如果為負值,表示讀取失敗*/ static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {int retvalue = 0;/* 向用戶空間發送數據 */memcpy(readbuf, kerneldata, sizeof(kerneldata));retvalue = copy_to_user(buf, readbuf, cnt);//cnt大小由應用程序指定if(retvalue == 0){printk("kernel senddata ok!\r\n");}else{printk("kernel senddata failed!\r\n");}//printk("chrdevbase read!\r\n");return 0; }/** @description : 向設備寫數據 * @param - filp : 設備文件,表示打開的文件描述符* @param - buf : 要寫給設備寫入的數據* @param - cnt : 要寫入的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 寫入的字節數,如果為負值,表示寫入失敗*/ static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {int retvalue = 0;/* 接收用戶空間傳遞給內核的數據并且打印出來 */retvalue = copy_from_user(writebuf, buf, cnt);if(retvalue == 0){printk("kernel recevdata:%s\r\n", writebuf);}else{printk("kernel recevdata failed!\r\n");}//printk("chrdevbase write!\r\n");return 0; }/** @description : 關閉/釋放設備* @param - filp : 要關閉的設備文件(文件描述符)* @return : 0 成功;其他 失敗*/ static int chrdevbase_release(struct inode *inode, struct file *filp) {//printk("chrdevbase release!\r\n");return 0; }/** 設備操作函數結構體*/ static struct file_operations chrdevbase_fops = {.owner = THIS_MODULE, .open = chrdevbase_open, //打開.read = chrdevbase_read, //讀.write = chrdevbase_write, //寫.release = chrdevbase_release, //關閉 };/** @description : 驅動入口函數 * @param : 無* @return : 0 成功;其他 失敗*/ static int __init chrdevbase_init(void) {int retvalue = 0;/* 注冊字符設備驅動 */retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);if(retvalue < 0){printk("chrdevbase driver register failed\r\n");}printk("chrdevbase init!\r\n");return 0; }/** @description : 驅動出口函數* @param : 無* @return : 無*/ static void __exit chrdevbase_exit(void) {/* 注銷字符設備驅動 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase exit!\r\n"); }/* * 將上面兩個函數指定為驅動的入口和出口函數 */ module_init(chrdevbase_init); module_exit(chrdevbase_exit);/* * LICENSE和作者信息*/ MODULE_LICENSE("GPL"); MODULE_AUTHOR("zuozhongkai");第32~36 行,chrdevbase_open 函數,當應用程序調用open 函數的時候此函數就會調用,本例程中我們沒有做任何工作,只是輸出一串字符,用于調試。這里使用了printk 來輸出信息,而不是printf!因為在Linux 內核中沒有printf 這個函數,printf運行在用戶態,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: Log Shutdown Reason\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 7CONSOLE_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 函數原型如下:
參數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 和作者信息。
編寫測試APP(1表示向驅動讀數據、2表示向驅動寫數據)
1、C 庫文件操作基本函數
編寫測試APP 就是編寫Linux 應用,需要用到C 庫里面和文件操作有關的一些函數,比如open、read、write 和close 這四個函數。
2、編寫測試APP 程序
驅動編寫好以后是需要測試的,一般編寫一個簡單的測試APP,測試APP 運行在用戶空間。測試APP 很簡單通過輸入相應的指令來對chrdevbase 設備執行讀或者寫操作。在1_chrdevbase 目錄中新建chrdevbaseApp.c 文件,在此文件中輸入如下內容:
#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" /*************************************************************** Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : chrdevbaseApp.c 作者 : 左忠凱 版本 : V1.0 描述 : chrdevbase驅測試APP。 其他 : 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>argv[2] 1:讀文件argv[2] 2:寫文件 論壇 : www.openedv.com 日志 : 初版V1.0 2019/1/30 左忠凱創建 ***************************************************************/static char usrdata[] = {"usr data!"};/** @description : main主程序* @param - argc : argv數組元素個數* @param - argv : 具體參數* @return : 0 成功;其他 失敗*/ int main(int argc, char *argv[]) {int fd, retvalue;char *filename;char readbuf[100], writebuf[100];if(argc != 3){printf("Error Usage!\r\n");return -1;}filename = argv[1];/* 打開驅動文件 */fd = open(filename, O_RDWR);if(fd < 0){printf("Can't open file %s\r\n", filename);return -1;}if(atoi(argv[2]) == 1){ /* 從驅動文件讀取數據 */retvalue = read(fd, readbuf, 50);if(retvalue < 0){printf("read file %s failed!\r\n", filename);}else{/* 讀取成功,打印出讀取成功的數據 */printf("read data:%s\r\n",readbuf);}}if(atoi(argv[2]) == 2){/* 向設備驅動寫數據 */memcpy(writebuf, usrdata, sizeof(usrdata));retvalue = write(fd, writebuf, 50);if(retvalue < 0){printf("write file %s failed!\r\n", filename);}}/* 關閉設備 */retvalue = close(fd);if(retvalue < 0){printf("Can't close file %s\r\n", filename);return -1;}return 0; }第21 行,數組usrdata 是測試APP 要向chrdevbase 設備寫入的數據。
第35 行,判斷運行測試APP 的時候輸入的參數是不是為3 個,main 函數的argc 參數表示參數數量,argv[]保存著具體的參數,如果參數不為3 個的話就表示測試APP 用法錯誤。比如,現在要從chrdevbase 設備中讀取數據,需要輸入如下命令:
上述命令一共有三個參數“./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 內容還是很簡單的,就是最普通的文件打開、關閉和讀寫操作。
Makefile編譯驅動程序和測試APP
1、編譯驅動程序
首先編譯驅動程序,也就是chrdevbase.c 這個文件,我們需要將其編譯為.ko 模塊,創建Makefile 文件,然后在其中輸入如下內容:
KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek CURRENT_PATH := $(shell pwd) obj-m := chrdevbase.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean:$(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”命令編譯驅動模塊,編譯過程如圖40.4.3.1 所示:
編譯成功以后就會生成一個叫做chrdevbaes.ko 的文件,此文件就是chrdevbase 設備的驅動模塊。至此,chrdevbase 設備的驅動就編譯成功。
2、編譯測試APP
測試APP 比較簡單,只有一個文件,因此就不需要編寫Makefile 了,直接輸入命令編譯。
因為測試APP 是要在ARM 開發板上運行的,所以需要使用arm-linux-gnueabihf-gcc 來編譯,輸入如下命令:
編譯完成以后會生成一個叫做chrdevbaseApp 的可執行程序,輸入如下命令查看chrdevbaseAPP 這個程序的文件信息:
file chrdevbaseApp結果如圖40.4.3.2 所示:
從圖40.4.3.2 可以看出,chrdevbaseAPP 這個可執行文件是32 位LSB 格式,ARM 版本的,因此chrdevbaseAPP 只能在ARM 芯片下運行。
運行測試
1、設置內核啟動、復制.ko和app到指定目錄、加載驅動模塊
驅動模塊chrdevbase.ko 和測試軟件chrdevbaseAPP 都已經準備好了,接下來就是運行測試。
為了方便測試,Linux 系統選擇通過TFTP 從網絡啟動,并且使用NFS 掛載網絡根文件系統,確保uboot 中bootcmd 環境變量的值為:
tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000bootargs 環境變量的值為:
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”這個目錄,如果沒有的話自行創建。注意,“/lib/modules/4.1.15”這個目錄用來存放驅動模塊,使用modprobe 命令加載驅動模塊的時候,驅動模塊要存放在此目錄下。“/lib/modules”是通用的,
不管你用的什么板子、什么內核,這部分是一樣的。不一樣的是后面的“4.1.15”,這里要根據你所使用的Linux 內核版本來設置,比如ALPHA 開發板現在用的是4.1.15 版本的Linux 內核,因此就是“/lib/modules/4.1.15”。如果你使用的其他版本內核,比如5.14.31,那么就應該創建
“/lib/modules/5.14.31”目錄,否則modprobe 命令無法加載驅動模塊。
因為是通過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 這兩個文件,如圖40.4.4.1 所示:
輸入如下命令加載chrdevbase.ko 驅動文件:
或
modprobe chrdevbase.ko【注意】如果使用modprobe 加載驅動的話,第一次可能會出現如圖40.4.4.2 所示的提示:
從圖40.4.4.2 可以看出,modprobe 提示無法打開“modules.dep”這個文件,因此驅動掛載失敗了。我們不用手動創建modules.dep 這個文件,直接輸入
depmod命令即可自動生成modules.dep,有些根文件系統可能沒有depmod 這個命令,如果沒有這個命令就只能重新配置busybox,使能此命令,然后重新編譯busybox。輸入“depmod”命令以后會自動生成modules.alias、
modules.symbols 和modules.dep 這三個文件,如圖40.4.4.3 所示:
重新使用modprobe 加載chrdevbase.ko,結果如圖40.4.4.4 所示:
從圖40.4.4.4 可以看到“chrdevbase init!”這一行,這一行正是chrdevbase.c 中模塊入口函數chrdevbase_init 輸出的信息,說明模塊加載成功!
輸入“lsmod”命令即可查看當前系統中存在的模塊,結果如圖40.4.4.5 所示:
從圖40.4.4.5 可以看出,當前系統只有“chrdevbase”這一個模塊。輸入如下命令查看當前系統中有沒有chrdevbase 這個設備:
結果如圖40.4.4.6 所示:
從圖40.4.4.6 可以看出,當前系統存在chrdevbase 這個設備,主設備號為200,跟我們設置的主設備號一致。
2、創建設備節點文件(mknod應用程序通過設備節點操作具體設備)
驅動加載成功需要在/dev 目錄下創建一個與之對應的設備節點文件,應用程序就是通過操作這個設備節點文件來完成對具體設備的操作。輸入如下命令創建/dev/chrdevbase 這個設備節點文件:
mknod /dev/chrdevbase c 200 0其中:
- “mknod”是創建節點命令
- “/dev/chrdevbase”是要創建的節點文件
- “c”表示這是個字符設備
- “200”是設備的主設備號
- “0”是設備的次設備號。
創建完成以后就會存在/dev/chrdevbase 這個文件,可以使用“ls /dev/chrdevbase -l”命令查看,結果如圖40.4.4.7 所示:
如果chrdevbaseAPP 想要讀寫chrdevbase 設備,直接對/dev/chrdevbase 進行讀寫操作即可,相當于/dev/chrdevbase 這個文件是chrdevbase 設備在用戶空間中的實現。Linux 下一切皆文件!
3、chrdevbase設備操作測試
使用chrdevbaseApp 軟件操作chrdevbase 這個設備,看看讀寫是否正常,首先進行讀操作,輸入如下命令:
./chrdevbaseApp /dev/chrdevbase 1結果如圖40.4.4.8 所示:
從圖40.4.4.8 可以看出,首先輸出“kernel senddata ok!”這一行信息,這是驅動程序中chrdevbase_read 函數輸出的信息,因為chrdevbaseAPP 使用read 函數從chrdevbase 設備讀取數據,因此chrdevbase_read 函數就會執行。chrdevbase_read 函數向chrdevbaseAPP 發送“kernel data!”數據,chrdevbaseAPP 接收到以后就打印出來,“read data:kernel data!”就是chrdevbaseAPP打印出來的接收到的數據。說明對chrdevbase 的讀操作正常,接下來測試對chrdevbase 設備的寫操作,輸入如下命令:
結果如圖40.4.4.9 所示:
只有一行“kernel recevdata:usr data!”,這個是驅動程序中的chrdevbase_write 函數輸出的。
chrdevbaseAPP 使用write 函數向chrdevbase 設備寫入數據“usr data!”。chrdevbase_write 函數接收到以后將其打印出來。說明對chrdevbase 的寫操作正常,既然讀寫都沒問題,說明我們編寫的chrdevbase 驅動是沒有問題的。
4、卸載驅動模塊
如果不再使用某個設備的話可以將其驅動卸載掉,比如輸入如下命令卸載掉chrdevbase 這個設備:
rmmod chrdevbase.ko卸載以后使用lsmod 命令查看chrdevbase 這個模塊還存不存在,結果如圖40.4.4.10 所示:
從圖40.4.4.10 可以看出,此時系統已經沒有任何模塊了,chrdevbase 這個模塊也不存在了,說明模塊卸載成功。
至此,chrdevbase 這個設備的整個驅動就驗證完成了。本章我們詳細的講解了字符設備驅動的開發步驟,并且以一個虛擬的chrdevbase 設備為例,完成了字符設備驅動的開發框架以及測試方法,以后的字符設備驅動實驗基本都以此為藍本。
總結
以上是生活随笔為你收集整理的虚拟字符设备驱动开发步骤的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android 4.4 生僻字,Andr
- 下一篇: 亲和数 杭电2040