從單片機到ARM Linux驅動——Linux驅動入門篇

       大一到大二這段時間裏學習過單片機的相關知識,對單片機有必定的認識和了解。若是要深究其原理可能還差了一些火候。知道如何編寫程序來點量一個LED燈,改一改官方提供的例程來實現一些功能作一些小東西,對IIC、SPI底層的通訊協議有必定的瞭解,可是學着學着逐漸以爲單片機我也就只能改改代碼了(固然有的代碼也不必定能改出來)。對於我這種之後不想從事單片機開發想搬磚的碼農來講已經差很少了(僅僅是我的觀點)。
       在單片機開發中咱們經常用到的是裸機,並無用到操做系統(或者接觸過ucos/rtos這種實時操做系統),可是嵌入式Linux開發就必須得在Linux系統中進行操做。咱們須要熟悉Linux操做系統,知道Linux的經常使用命令、文件系統、Linux網絡、多線程/多進程,同時要會用vi編輯器、gcc編譯器、shell腳本和一些簡單的makefile的編寫,在這些的基礎之上進行Linux驅動開發的學習就會如步青雲。node

往期推薦:
       史上最全的Linux經常使用命令彙總(超全面!超詳細!)收藏這一篇就夠了!
       STM32經過PWM產生頻率爲20HZ佔空比爲50%方波,並經過單片機測量頻率並顯示在這裏插入圖片描述linux

       嵌入式Linux操做系統具備:開放源碼、所需容量小(最小的安裝大約須要2MB)、不需著做權費用、成熟與穩定(經歷這些年的發展與使用)、良好的支持等特色。所以被普遍應用於移動電話、我的數碼等產品中。嵌入式Linux開發主要包括:底層驅動、操做系統內核、應用開發三大類。須要掌握系統移植(Uboot、Linux Kernel的移植和裁剪、根文件系統的構建)、Linux驅動及內核開發(字符設備驅動、塊設備驅動、網絡設備驅動)應用開發因爲博主能力有限所瞭解的也很少。web

字符設備驅動簡介

       字符設備是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 內核驅動操做函數集合shell

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 驅動開發重點是學習其驅動框架api

驅動模塊的加載和卸載

       Linux 驅動有兩種運行方式第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啓動的時候就會自動運行驅動程序第二種就是將驅動編譯成模塊(Linux 下模塊擴展名爲.ko),在Linux 內核啓動之後使用「insmod」命令加載驅動模塊。在調試驅動的時候通常都選擇將其編譯爲模塊,這樣咱們修改驅動之後只須要編譯一下驅動代碼便可,不須要編譯整個 Linux 代碼。並且在調試的時候只須要加載或者卸載驅動模塊便可,不須要重啓整個系統。bash

       模塊有加載和卸載兩種操做,咱們在編寫驅動的時候須要註冊這兩種操做函數,模塊的加載和卸載註冊函數以下:網絡

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,有兩種命令能夠加載驅動模塊: insmodmodprobe,insmod是最簡單的模塊加載命令,此命令用於加載指定的.ko 模塊,好比加載 drv.ko 這個驅動模塊,命令以下:app

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 這個設備有以下兩個要求:
一、可以對 chrtest 進行打開和關閉操做
       設備打開和關閉是最基本的要求,幾乎全部的設備都得提供打開和關閉的功能。所以咱們須要實現 file_operations 中的 open 和 release 這兩個函數。
二、對 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_openchrtest_readchrtest_writechrtest_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 類型的設備號。

設備號的分配

一、靜態分配設備號

註冊字符設備的時候須要給設備指定一個設備號,這個設備號能夠是驅動開發者靜態的指定一個設備號,好比選擇 200 這個主設備號。有一些經常使用的設備號已經被 Linux 內核開發者給分配掉了,具體分配的內容能夠查看文檔 Documentation/devices.txt。並非說內核開發者已經分配掉的主設備號咱們就不能用了,具體能不能用還得看咱們的硬件平臺運行過程當中有沒有使用這個主設備號,使用cat /proc/devices命令便可查看當前系統中全部已經使用了的設備號。

二、動態分配設備號

靜態分配設備號須要咱們檢查當前系統中全部被使用了的設備號,而後挑選一個沒有使用的。並且靜態分配設備號很容易帶來衝突問題, 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 開始,要釋放的設備號數量。

       不積小流無以成江河,不積跬步無以致千里。而我想要成爲萬里羊,就必須堅持學習來獲取更多知識,用知識來改變命運,用博客見證成長,用行動證實我在努力。
       若是個人博客對你有幫助、若是你喜歡個人博客內容,記得「點贊」 「評論」 「收藏」一鍵三連哦!據說點讚的人運氣不會太差,每一天都會元氣滿滿呦!若是實在要白嫖的話,那祝你開心每一天,歡迎常來我博客看看。
在這裏插入圖片描述