大一到大二這段時間里學習過單片機的相關知識,對單片機有一定的認識和了解,如果要深究其原理可能還差了一些火候,知道如何撰寫程式來點量一個LED燈,改一改官方提供的例程來實作一些功能做一些小東西,對IIC、SPI底層的通信協議有一定的了解,但是學著學著逐漸覺得單片機我也就只能改改代碼了(當然有的代碼也不一定能改出來),對于我這種以后不想從事單片機開發想搬磚的碼農來說已經差不多了(僅僅是個人觀點),
在單片機開發中我們常常用到的是裸機,并沒有用到作業系統(或者接觸過ucos/rtos這種實時作業系統),但是嵌入式Linux開發就必須得在Linux系統中進行操作,我們需要熟悉Linux作業系統,知道Linux的常用命令、檔案系統、Linux網路、多執行緒/多行程,同時要會用vi編輯器、gcc編譯器、shell腳本和一些簡單的makefile的撰寫,在這些的基礎之上進行Linux驅動開發的學習就會如步青云,往期推薦:
史上最全的Linux常用命令匯總(超全面!超詳細!)收藏這一篇就夠了!
STM32通過PWM產生頻率為20HZ占空比為50%方波,并通過單片機測量頻率并顯示
嵌入式Linux作業系統具有:開放原始碼、所需容量小(最小的安裝大約需要2MB)、不需著作權費用、成熟與穩定(經歷這些年的發展與使用)、良好的支持等特點,因此被廣泛應用于移動電話、個人數碼等產品中,嵌入式Linux開發主要包括:底層驅動、作業系統內核、應用開發三大類,需要掌握系統移植(Uboot、Linux Kernel的移植和裁剪、根檔案系統的構建)、Linux驅動及內核開發(字符設備驅動、塊設備驅動、網路設備驅動)應用開發由于博主能力有限所了解的也不多,
文章目錄
- 字符設備驅動簡介
- 字符設備驅動開發步驟
- 驅動模塊的加載和卸載
- 字符設備注冊與注銷
- 實作設備的具體操作函式
- 添加LICENSE和作者資訊
- Linux設備號
- 設備號的組成
- 設備號的分配
字符設備驅動簡介
字符設備是Linux驅動中最基本的一類設備驅動,字符設備就是一個位元組,按照位元組進行讀寫操作設備,讀寫資料是分先后順序的,比如我們常見的點燈、按鍵、IIC、SPI、LCD等都是字符設備,這些設備的驅動就叫做字符設備驅動,
在Linux中開發一般只能是用戶態,也就是用戶只能撰寫應用程式,但是要作用于內核,那么就需要了解Linux中應用程式是如何呼叫內核中的驅動程式的,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 函式的時候流程如圖所示:

應用程式使用到的函式在具體的驅動中都有與之對應的函式,比如應用程式中呼叫了 open 這個函式,那么在驅動程式中也得有一個名為 open 的函式,每一個系統呼叫,在驅動中都有與之對應的一個驅動函式,在 Linux 內核檔案 include/linux/fs.h 中有個叫做 file_operations 的結構體,此結構體就是 Linux 內核驅動操作函式集合,
struct file_operations {
struct module *owner;//owner 擁有該結構體的模塊的指標,一般設定為 THIS_MODULE
loff_t (*llseek) (struct file *, loff_t, int);//llseek 函式用于修改檔案當前的讀寫位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t*);//read 函式用于讀取設備檔案
ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *);//write 函式用于向設備檔案寫入(發送)資料
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*);//poll 是個輪詢函式,用于查詢設備是否可以進行非阻塞的讀寫
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//unlocked_ioctl 函式提供對于設備的控制功能,與應用程式中的 ioctl 函式對應,
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);//compat_ioctl 函式與 unlocked_ioctl 函式功能一樣,區別在于在 64 位系統上,32 位的應用程式呼叫將會使用此函式,在 32 位的系統上運行 32 位的應用程式呼叫的是unlocked_ioctl,
int (*mmap) (struct file *, struct vm_area_struct *);//mmap 函式用于將將設備的記憶體映射到行程空間中(也就是用戶空間),一般幀緩沖設備會使用此函式,比如 LCD 驅動的顯存,將幀緩沖(LCD 顯存)映射到用戶空間中以后應用程式就可以直接操作顯存了,這樣就不用在用戶空間和內核空間之間來回復制,
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);//open 函式用于打開設備檔案,
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);//release 函式用于釋放(關閉)設備檔案,與應用程式中的 close 函式對應,
int (*fsync) (struct file *, loff_t, loff_t, int datasync);//fasync 函式用于重繪待處理的資料,用于將緩沖區中的資料重繪到磁盤中,
int (*aio_fsync) (struct kiocb *, int datasync);//aio_fsync 函式與 fasync 函式的功能類似,只是 aio_fsync 是異步重繪待處理的資料
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
};
字符設備驅動開發步驟
在學習裸機或者 STM32 的時候關于驅動的開發就是初始化相應的外設暫存器,在 Linux 驅動開發中肯定也是要初始化相應的外設暫存器,這是毫無疑問的,只是在 Linux 驅動開發中我們需要按照其規定的框架來撰寫驅動,所以說學 Linux 驅動開發重點是學習其驅動框架,
驅動模塊的加載和卸載
Linux 驅動有兩種運行方式,第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啟動的時候就會自動運行驅動程式,第二種就是將驅動編譯成模塊(Linux 下模塊擴展名為.ko),在Linux 內核啟動以后使用“insmod”命令加載驅動模塊,在除錯驅動的時候一般都選擇將其編譯為模塊,這樣我們修改驅動以后只需要編譯一下驅動代碼即可,不需要編譯整個 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 函式就會被呼叫,字符設備驅動模塊加載和卸載模板如下所示:
/* 驅動入口函式 */
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 命令相比 insmod 要智能一些, modprobe 命令主要智能在提供了模塊的依賴性分析、錯誤檢查、錯誤報告等功能,推薦使用 modprobe 命令來加載驅動, modprobe 命令默認會去/lib/modules/目錄中查找模塊,比如本書使用的 Linux kernel 的版本號為 4.1.15,因此 modprobe 命令默認到/lib/modules/4.1.15 這個目錄中查找相應的驅動模塊,一般自己制作的根檔案系統中是不會有這個目錄的,所以需要自己手動創建,驅動模塊的卸載使用命令“rmmod”即可,比如要卸載 drv.ko,使用如下命令即可:
rmmod drv.ko
也可以使用“modprobe -r”命令卸載驅動,比如要卸載 drv.ko,命令如下:
modprobe -r drv.ko
使用 modprobe 命令可以卸載掉驅動模塊所依賴的其他模塊,前提是這些依賴模塊已經沒有被其他模塊所使用,否則就不能使用 modprobe 來卸載驅動模塊,所以對于模塊的卸載,還是推薦使用 rmmod 命令,
字符設備注冊與注銷
對于字符設備驅動而言,當驅動模塊加載成功以后需要注冊字符設備,同樣,卸載驅動模塊的時候也需要注銷掉字符設備,字符設備的注冊和注銷函式原型如下所示:
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);
- 以上代碼中,一開始定義了一個 file_operations 結構體變數
test_fops, test_fops 就是設備的操作函式集合,只是此時我們還沒有初始化 test_fops 中的 open、 release 等這些成員變數,所以這個操作函式集合還是空的, - 第十行,呼叫函式 register_chrdev 注冊字符設備,主設備號為 200,設備名字為“chrtest”,設備操作函式集合就是第 1 行定義的 test_fops,要注意的一點就是,選擇沒有被使用的主設備號,輸入命令
cat /proc/devices可以查看當前已經被使用掉的設備號, - 第二十一行,呼叫函式 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 這兩個函式,需求很清晰了,修改驅動示例代碼在其中加入 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);
- 在上面代碼中,我們一開始撰寫了四個函式:
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() //添加模塊作者資訊
給示例代碼加入 LICENSE 和作者資訊,完成以后的內容如下:
/* 打開設備 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
/* 用戶實作具體功能 */
return 0;
}
......
/* 將上面兩個函式指定為驅動的入口和出口函式 */
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");//LICENSE 采用 GPL 協議,
MODULE_AUTHOR("wly");//添加作者名字
當添加完作者和LICENSE和作者資訊后,字符設備驅動的完整流程就基本上結束了,并且也提供了一個完整的Linux驅動的模板,以后字符設備驅動開發就可以修改這個模板,
Linux設備號
Linux的設備管理是和檔案系統緊密結合的,各種設備都以檔案的形式存放在/dev目錄下,稱為設備檔案,應用程式可以打開、關閉和讀寫這些設備檔案,完成對設備的操作,就像操作普通的資料檔案一樣,為了管理這些設備,系統為設備編了號,這個號就被稱為Linux設備號!
設備號的組成
設備號由主設備號和次設備號兩部分組成,主設備號表示某一個具體的驅動,次設備號表示使用這個驅動的各個設備, Linux 提供了一個名為 dev_t 的資料型別表示設備號, dev_t 定義在檔案include/linux/types.h 里面,定義如下:
typedef __u32 __kernel_dev_t;
......
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 中提供了幾個關于設備號的操作函式(本質是宏),如下所示:
#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 位的值即可,
- 第 5 行,宏 MKDEV 用于將給定的主設備號和次設備號的值組合成 dev_t 型別的設備號,
設備號的分配
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)
- dev:保存申請到的設備號,
- baseminor: 次設備號起始地址, alloc_chrdev_region 可以申請一段連續的多個設備號,這些設備號的主設備號一樣,但是次設備號不同,次設備號以 baseminor 為起始地址地址開始遞增,一般 baseminor 為 0,也就是說次設備號從 0 開始,
- count: 要申請的設備號數量,
- name:設備名字,
注銷字符設備之后要釋放掉設備號,設備號釋放函式如下:
void unregister_chrdev_region(dev_t from, unsigned count)
- from:要釋放的設備號,
- count: 表示從 from 開始,要釋放的設備號數量,
不積小流無以成江河,不積跬步無以至千里,而我想要成為萬里羊,就必須堅持學習來獲取更多知識,用知識來改變命運,用博客見證成長,用行動證明我在努力,
如果我的博客對你有幫助、如果你喜歡我的博客內容,記得“點贊” “評論” “收藏”一鍵三連哦!聽說點贊的人運氣不會太差,每一天都會元氣滿滿呦!如果實在要白嫖的話,那祝你開心每一天,歡迎常來我博客看看,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/192599.html
標籤:其他


