【Linux筆記】嵌入式Linux驅動基礎(通俗易懂)

上一篇分享的:從單片機工程師的角度看嵌入式Linux中有簡單提到Linux的三大類驅動:

======001

我們學習編程的時候都會從hello程序開始。同樣的,學習Linux驅動我們也從最簡單的hello驅動學起。

驅動層和應用層

還記得實習那會兒我第一次接觸嵌入式Linux項目的時候,我的導師讓我去學習項目的其它模塊,然後嘗試着寫一個串口相關的應用。那時候知道可以把設備當做文件來操作,但是不知道爲什麼是這樣,就去網上搜了一些代碼(驅動代碼),然後和我的應用代碼放在同一個文件裏。

給導師看了之後,導師說那些驅動程序不需要我寫,那些驅動已經寫好被編譯到內核裏了,可以直接用了,我只需關注應用層就好了。我當時腦子裏就在打轉。。what?

STM32用一個串口不就是串口初始化,然後想怎麼用就怎麼用嗎?後來經過學習才知道原來是那麼一回事呀。這就是單片機轉轉嵌入式Linux的思維誤區之一。學嵌入式Linux之前我們有必要暫時忘了我們單片機的開發方式,重新梳理嵌入式Linux的開發流程。下面看一下STM32裸機開發與嵌入式Linux開發的一些區別:

======002

======003

======004

嵌入式Linux的開發方式與STM32裸機開發的方式有點不一樣。在STM32的裸機開發中,驅動層與應用層的區分可能沒有那麼明顯,常常都雜揉在一起。

當然,有些很有水平的裸機程序分層分得還是很明顯的。但是,在嵌入式Linux中,驅動和應用的分層是特別明顯的,最直觀的感受就是驅動程序是一個.c文件裏,應用程序是另一個.c文件。

比如我們這個hello驅動實驗中,我們的驅動程序爲hello_drv.c、應用程序爲hello_app.c。

驅動模塊的加載有兩種方式:

第一種方式是動態加載的方式,即驅動程序與內核分開編譯,在內核運行的過程中加載;

第二種方式是靜態加載的方式,即驅動程序與內核一同編譯,在內核啓動過程中加載驅動。在調試驅動階段常常選用第一種方式,因爲較爲方便;在調試完成之後才採用第二種方式與內核一同編譯。

STM32裸機開發與嵌入式Linux開發還有一點不同的就是:STM32裸機開發最終要燒到板子的常常只有一個文件(除開含有IAP程序的情況或者其它情況),嵌入式Linux就需要分開編譯、燒寫。

Linux字符設備驅動框架

我們先看一個圖:

======005

當我們的應用在調用open、close、write、read等函數時,爲什麼就能操控硬件設備。那是因爲有驅動層在支撐着與硬件相關的操作,應用程序在調用打開、關閉、讀、寫等操作會觸發相應的驅動層函數。

本篇筆記我們以hello驅動做分享,hello驅動屬於字符設備。實現的驅動函數大概是怎麼樣的是有套路可尋的,這個套路在內核文件include/linux/fs.h中,這個文件中有如下結構體:

======006

這個結構體裏的成員都是些函數指針變量,我們需要根據實際的設備確定我們需要創建哪些驅動函數實體。比如我們的hello驅動的幾個基本的函數(打開/關閉/讀/寫)可創建爲(以下代碼來自:百問網):

(1)打開操作

static int hello_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

打開函數的兩個形參的類型要與struct file_operations結構體裏open成員的形參類型一致,裏面有一句打印語句,方便直觀地看到驅動的運行過程。

(2)關閉操作

static int hello_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

(3)讀操作

static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));
	return MIN(1024, size);
}

copy_to_user函數的原型爲:

static inline int copy_to_user(void __user *to, const void *from, unsigned long n);

用該函數來讀取內核空間(kernel_buf)的數據給到用戶空間(buf)。 另外,kernel_buf的定義如下:

static char kernel_buf[1024];

MIN爲宏:

#define MIN(a, b) (a < b ? a : b)

MIN(1024, size)作爲copy_to_user的實參意在對拷貝的數據長度做限制(不能超出kernel_buf的大小)。

(4)寫操作

static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}

copy_from_user函數的原型爲:

static inline int copy_from_user(void *to,const void __user volatile *from,unsigned long n)

用該函數來將用戶空間(buf)的數據傳送到內核空間(kernel_buf)。

有了這些驅動函數,就可以給到一個struct file_operations類型的結構體變量hello_drv,如:

static struct file_operations hello_drv = 
{
    .owner   = THIS_MODULE,
    .open    = hello_drv_open,
    .read    = hello_drv_read,
    .write   = hello_drv_write,
    .release = hello_drv_close,
};

有些朋友可能沒見過這種結構體初始化的形式(結構體成員前面加個.號),可以去看往期筆記:指定初始化器進行了解【C語言筆記】結構體

上面這個結構體變量hello_drv容納了我們hello設備的驅動接口,最終我們要把這個hello_drv註冊給Linux內核,套路就是這樣的:把驅動程序註冊給內核,之後我們的應用程序就可以使用open/close/write/read等函數來操控我們的設備,Linux內核在這裏起到一箇中間人的作用,把兩頭的驅動與應用協調得很好。

我們前面說了驅動的裝載方式之一的動態裝載:把驅動程序編譯成模塊,再動態裝載。動態裝載的體現就是開發板已經啓動運行了Linux內核,我們通過開發板串口終端使用命令來裝載驅動。裝載驅動有兩個命令,比如裝載我們的hello驅動:

方法一:insmod hello_drv.ko
方法二:modprobe hello_drv.ko

其中modprobe命令不僅能裝載當前驅動,而且還會同時裝載與當前驅動相關的依賴驅動。有了轉載就有卸載,也有兩種方式:

方法一:rmmod hello_drv.ko
方法二:modprobe -r hello_drv.ko

其中modprobe命令不僅卸載當前驅動,也會同時卸載依賴驅動。

我們在串口終端調用裝載與卸載驅動的命令,怎麼就會執行裝載與卸載操作。對應到驅動程序裏我們有如下兩個函數:

module_init(hello_init); //註冊模塊加載函數
module_exit(hello_exit); //註冊模塊卸載函數

這裏加載與註冊有用到hello_inithello_exit函數,我們前面說的把hello_drv驅動註冊到內核就是在hello_init函數裏做,如:

static int __init hello_init(void)
{
	int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /* 註冊hello驅動 */
	major = register_chrdev(0, 			  /* 主設備號,爲0則系統自動分配 */
                            "hello", 	  /* 設備名稱 */
                            &hello_drv);  /* 驅動程序 */
    
	/* 下面操作是爲了在/dev目錄中生成一個hello設備節點 */
    /* 創建一個類 */
	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hello");
		return -1;
	}
	
    /* 創建設備,該設備創建在hello_class類下面 */
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
	
	return 0;
}

這裏這個驅動程序入口函數hello_init中註冊完驅動程序之後,同時通過下面連個創建操作來創建設備節點,即在/dev目錄下生成設備文件。

據我瞭解,在之前版本的Linux內核中,設備節點需要手動創建,即通過創建節點命令mknod 在/dev目錄下自己手動創建設備文件。既然已經有新的方式創建節點了,這裏就不摳之前的內容了。

以上就是分享關於驅動一些內容,通過以上分析,我們知道,其是有套路(就是常說的驅動框架)可尋的,比如:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* 其她頭文件...... */

/* 一些驅動函數 */
static ssize_t xxx_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{

}

static ssize_t xxx_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{

}

static int xxx_open (struct inode *node, struct file *file)
{

}

static int xxx_close (struct inode *node, struct file *file)
{

}
/* 其它驅動函數...... */

/* 定義自己的驅動結構體 */
static struct file_operations xxx_drv = {
	.owner	 = THIS_MODULE,
	.open    = xxx_open,
	.read    = xxx_read,
	.write   = xxx_write,
	.release = xxx_close,
	/* 其它程序......... */
};

/* 驅動入口函數 */
static int __init xxx_init(void)
{

}

/* 驅動出口函數 */
static void __exit hello_exit(void)
{

}

/* 模塊註冊與卸載函數 */
module_init(xxx_init);
module_exit(xxx_exit);

/* 模塊許可證(必選項) */
MODULE_LICENSE("GPL");

按照這樣的套路來開發驅動程序的,有套路可尋那就比較好學習了,至少不會想着怎麼起函數名而煩惱,按套路來就好,哈哈

關於驅動的知識,這篇筆記中還可以展開很多內容,限於篇幅就不展開了。我們之後再進行學習、分享。下面看一下測試程序/應用程序(hello_drv_test.c中的內容,以下代碼來自:百問網):

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./hello_drv_test -w abc
 * ./hello_drv_test -r
 */
int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	
	/* 1. 判斷參數 */
	if (argc < 2) 
	{
		printf("Usage: %s -w <string>\n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}

	/* 2. 打開文件 */
	fd = open("/dev/hello", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}

	/* 3. 寫文件或讀文件 */
	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	{
		len = strlen(argv[2]) + 1;
		len = len < 1024 ? len : 1024;
		write(fd, argv[2], len);
	}
	else
	{
		len = read(fd, buf, 1024);		
		buf[1023] = '\0';
		printf("APP read : %s\n", buf);
	}
	
	close(fd);
	
	return 0;
}

就是一些讀寫操作,跟我們學習文件操作是一樣的。學單片機的有些朋友可能不太熟悉main函數的這種寫法:

int main(int argc, char **argv)

main函數在C中有好幾種寫法(可查看往期筆記:C語言main函數的幾種寫法),在Linux中常用這種寫法。argc與argv這兩個值可以從終端(命令行)輸入,因此這兩個參數也被稱爲命令行參數。argc爲命令行參數的個數,argv爲字符串命令行參數的首地址。

最後,我們把編譯生成的驅動模塊hello_drv.ko與應用程序hello_drv_test放到共享目錄錄nfs_share中,同時在開發板終端掛載共享目錄:

mount -t nfs -o nolock,vers=4 192.168.1.104:/home/book/nfs_share /mnt

關於nfs網絡文件系統的使用可查看往期筆記:[【Linux筆記】掛載網文件系統]。

然後我們通過insmod 命令裝載驅動,但是出現瞭如下錯誤:

======007

這是因爲我們的驅動的編譯依賴與內核版本,編譯用的內核版本與當前開發板運行的內核的版本不一致所以會產生該錯誤,重新編譯內核,並把編譯生成的Linux內核zImage映像文件與設備樹文件*.dts文件拷貝到開發板根文件系統的/boot目錄下,然後進行同步操作:

#mount -t nfs -o nolock,vers=4 192.168.1.114:/home/book/nfs_share /mnt
#cp /mnt/zImage /boot
#cp /mnt/.dtb /boot
#sync

最後,重啓開發板。最後,成功運行程序:

======008

下面是完整的hello驅動程序(來源:百問網):

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

/* 1. 確定主設備號                                                                 */
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;


#define MIN(a, b) (a < b ? a : b)

/* 3. 實現對應的open/read/write等函數,填入file_operations結構體                   */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));
	return MIN(1024, size);
}

static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}

static int hello_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static int hello_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 2. 定義自己的file_operations結構體                                              */
static struct file_operations hello_drv = {
	.owner	 = THIS_MODULE,
	.open    = hello_drv_open,
	.read    = hello_drv_read,
	.write   = hello_drv_write,
	.release = hello_drv_close,
};

/* 4. 把file_operations結構體告訴內核:註冊驅動程序                                */
/* 5. 誰來註冊驅動程序啊?得有一個入口函數:安裝驅動程序時,就會去調用這個入口函數 */
static int __init hello_init(void)
{
	int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */


	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hello");
		return -1;
	}
	
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
	
	return 0;
}

/* 6. 有入口函數就應該有出口函數:卸載驅動程序時,就會去調用這個出口函數           */
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(hello_class, MKDEV(major, 0));
	class_destroy(hello_class);
	unregister_chrdev(major, "hello");
}


/* 7. 其他完善:提供設備信息,自動創建設備節點                                     */

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");


在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章