一、字符設備驅動簡介
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 函數的時候流程如圖 40.1.2 所示:
應用程序使用到的函數在具體驅動程序中都有與之對應的函數,比如應用程序中調用了 open 這個函數,那么在驅動程序中也得有一個名為 open 的函數。
每一個系統調用,在驅動中都有與之對應的一個驅動函數,在 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 *, const char __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 *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
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);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
比較重要、常用的函數:
第 2 行:owner 擁有該結構體的模塊的指針,一般設置為 THIS_MODULE。
第 3 行:llseek 函數用于修改文件當前的讀寫位置。
第 4 行:read 函數用于讀取設備文件。
第 5 行:write 函數用于向設備文件寫入(發送)數據。
第 9 行:poll 是個輪詢函數,用于查詢設備是否可以進行非阻塞的讀寫。
第 10 行:unlocked_ioctl 函數提供對于設備的控制功能,與應用程序中的ioctl函數對應。
第 11 行:compat_ioctl 函數與 unlocked_ioctl 函數功能一樣,區別在于在 64 位系統上,32 位的應用程序調用將會使用此函數。在 32 位的系統上運行 32 位的應用程序調用的是 unlocked_ioctl。
第 12 行:mmap 函數用于將設備的內存映射到進程空間中(也就是用戶空間),一般幀緩沖設備會使用此函數,比如 LCD 驅動的顯存,將幀緩沖(LCD 顯存)映射到用戶空間中以后應用程序就可以直接操作顯存了,就不用在用戶空間和內核空間之間來回復制。
第 14 行:open 函數用于打開設備文件。
第 16 行:release 用于釋放(關閉)設備文件,與應用程序中 close 函數對應。
第 17 行:fasync 函數用于刷新待處理的數據,用于將緩沖區中的數據刷新到磁盤。
第 18 行:aio_fsync 函數與 fasync 函數的功能類似,只是 aio_fsync 是異步刷新待處理的數據。
二、字符設備驅動開發步驟
2.1 驅動模塊的加載和卸載
Linux 驅動有兩種運行方式,第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啟動的時候就會自動運行驅動程序。第二種就是將驅動編譯成模塊(Linux 下模塊擴展名為.ko),在 Linux 內核啟動以后使用“insmod”命令加載驅動模塊。
模塊有加載和卸載兩種操作,我們在編寫驅動的時候需要注冊這兩種操作函數,模塊的加載和卸載注冊函數如下:
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)
{
/* 入口函數具體內容 */
return 0;
}
/* 驅動出口函數 */
static void __exit xxx_exit(void)
{
/* 出口函數具體內容 */
}
/* 將上面兩個函數指定為驅動的入口和出口函數 */
module_init(xxx_init);
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 命令默認會去 /lib/modules/ 驅動模塊的卸載使用命令“rmmod”即可,比如要卸載 drv.ko,使用如下命令: rmmod drv.ko 也可以使用“modprobe -r”命令卸載驅動,比如要卸載 drv.ko,命令如下: modprobe -r drv.ko modprobe 命令可以卸載掉驅動模塊所依賴的其他模塊,前提是這些依賴模塊已經沒有被其他模塊所使用,否則就不能使用 modprobe 來卸載驅動模塊。所以對于模塊的卸載,推薦使用 rmmod 命令。 2.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 中進行。字符設備的注冊和注銷示例代碼如下所示: static struct file_operations test_fops; /* 驅動入口函數 */ static int __init xxx_init(void) { /* 入口函數具體內容 */ int retvalue = 0; /* 注冊字符設備驅動 */ retvalue = register_chrdev(200, 'chrtest', &test_fops); if(retvalue < 0){ /* 字符設備注冊失敗,自行處理 */ } return 0; } /* 驅動出口函數 */ static void __exit xxx_exit(void) { /* 注銷字符設備驅動 */ unregister_chrdev(200, 'chrtest'); } /* 將上面兩個函數指定為驅動的入口和出口函數 */ module_init(xxx_init); module_exit(xxx_exit); 2.3 實現設備的具體操作函數 1、對 chrtest 進行打開和關閉操作 設備打開和關閉是最基本的要求,幾乎所有的設備都得提供打開和關閉的功能。因此我們需要實現 file_operations 中的 open 和 release 這兩個函數。 2、對 chrtest 進行讀寫操作 假設 chrtest 設備控制著一段緩沖區(內存),應用程序通過 read 和 write 這兩個函數對 chrtest 的緩沖區進行讀寫操作,需要實現 file_operations 中的 read 和 write 這兩個函數。 加入 file_operations 結構體變量 test_fops 的初始化操,內容如下: /* 打開設備 */ static int chrtest_open(struct inode *inode, struct file *filp) { /* 用戶實現具體功能 */ return 0; } /* 從設備讀取 */ static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { /* 用戶實現具體功能 */ return 0; } /* 向設備寫數據 */ static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { /* 用戶實現具體功能 */ return 0; } /* 關閉/釋放設備 */ static int chrtest_release(struct inode *inode, struct file *filp) { /* 用戶實現具體功能 */ return 0; } static struct 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){ /* 字符設備注冊失敗,自行處理 */ } return 0; } /* 驅動出口函數 */ static void __exit xxx_exit(void) { /* 注銷字符設備驅動 */ unregister_chrdev(200, 'chrtest'); } /* 將上面兩個函數指定為驅動的入口和出口函數 */ module_init(xxx_init); module_exit(xxx_exit); 2.4 添加 LICENSE 和作者信息 LICENSE 和作者信息的添加使用如下兩個函數: MODULE_LICENSE() //添加模塊 LICENSE 信息 MODULE_AUTHOR() //添加模塊作者信息 三、Linux 設備號 3.1 設備號的組成 Linux 中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成,主設備號表示某一個具體的驅動,次設備號表示使用這個驅動的各個設備。Linux 提供了 一個 dev_t 的數據類型表示設備號,dev_t 定義在include/linux/types.h 里面,定義如下: typedef __u32 __kernel_dev_t; ...... typedef __kernel_dev_t dev_t; 其中__u32 定義在文件 include/uapi/asm-generic/int-ll64.h 里面: typedef unsigned int __u32; 因此 dev_t 是 unsigned int 類型,是一個 32 位的數據類型。這 32 位的數據構成了主設備號和次設備號,其中高 12 位為主設備號,低 20 位為次設備號。 在文件 include/linux/kdev_t.h 中提供了幾個關于設備號的操作函數(本質是宏),如下所示: #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) 第 1 行:宏 MINORBITS 表示次設備號位數,一共是 20 位。 第 2 行:宏 MINORMASK 表示次設備號掩碼。 第 3 行:宏 MAJOR 用于從 dev_t 中獲取主設備號,將 dev_t 右移 20 位即可。 第 4 行:宏 MINOR 用于從 dev_t 中獲取次設備號,取 dev_t 的低 20 位的值。 第 6 行:宏 MKDEV 用于將給定的主設備號和次設備號合成 dev_t 類型的設備號。 3.2 設備號的分配 1、靜態分配設備號 注冊字符設備的時候需要給設備指定一個設備號,這個設備號可以是驅動開發者靜態的指定一個設備號,比如選擇 200 這個主設備號。 靜態分配設備號需要我們檢查當前系統中所有被使用了的設備號,然后挑選一個沒有使用的。 2、動態分配設備號 在注冊字符設備之前先申請一個設備號,系統會自動給你一個沒有被使用的設備號,卸載驅動的時候釋放掉這個設備號即可。 設備號的申請函數如下: 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.1、添加頭文件路徑 編寫 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 的文件,此文件默認內容如下所示: { 'configurations': [ { 'name': 'Linux', 'includePath': [ '${workspaceFolder}/**', ], 'defines': [], 'compilerPath': '/usr/bin/clang',
上一篇:【IMX6ULL學習筆記】十一、LED字符設備
下一篇:【IMX6ULL學習筆記】九、Linux內核移植
推薦閱讀最新更新時間:2025-05-10 15:19




設計資源 培訓 開發板 精華推薦
- 使用 Richtek Technology Corporation 的 RT9986A 的參考設計
- 適用于STM32F439ZI MCU的STM32 Nucleo-144開發板,支持Arduino,ST Zio和morpho連接
- NCP1251GEVB,用于筆記本電腦的 25W、5V、12V AC 到 DC 多輸出電源的評估板
- 使用 Analog Devices 的 LT4276AHUFD 的參考設計
- 負載開關IC TCK321G、TCK322G、TCK323G應用&電路
- ADP2120 降壓穩壓器評估板
- RT9288A LED Driver with PWM Brightness Control (12V to 60V) 典型應用
- SILINKPS-EVB,用于電纜調制解調器應用的 Si3225 用戶線接口的評估板
- 用于線性歐姆表的 TL431A 可編程精密基準的典型應用
- 基于VIPer013BLS的5V/100mA高壓優化降壓轉換器