1. 程式人生 > >linux驅動由淺入深系列:驅動程式的基本結構概覽之一(第一個驅動程式)

linux驅動由淺入深系列:驅動程式的基本結構概覽之一(第一個驅動程式)

本系列導航:


提到linux驅動程式,首先應該知道它是linux的核心模組。那麼想要編寫驅動程式,就要首先認識一下linux的核心模組機制。Linux核心模組是使得複雜而龐大的linux核心條理清晰、可裁剪、高相容性的重要特性。

Linux核心模組的特點:

1,  模組本身不被編譯進核心映象,能夠控制核心的大小。

2,  模組可以在需要的時候中被動態載入,一旦載入完成就和核心其它部分完全一樣。

下面便是linux核心模組的helloworld程式,結構十分固定。編譯完成後生成hello.ko,通過insmod hello.ko進行載入,載入時輸出” hello module has been mount!”,使用rmmod hello進行解除安裝,解除安裝時輸出” hellomodule has been remove!”。

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Radia");

static int hello_init()
{
	printk(KERN_EMERG "hello module has been mount!\n");
	return 0;
}

static void hello_exit()
{
	printk(KERN_EMERG "hello module has been remove!\n");
}

module_init(hello_init);
module_exit(hello_exit);

在Linux下可以通過兩種方式載入驅動程式:靜態載入和動態載入。

靜態載入就是把驅動程式直接編譯進核心,系統啟動後可以直接呼叫。靜態載入的缺點是除錯起來比較麻煩,每次修改一個地方都要重新編譯和下載核心,效率較低。若採用靜態載入的驅動較多,會導致核心容量很大,浪費儲存空間。

動態載入利用了Linux的module特性,可以在系統啟動後用insmod命令新增模組(.ko),在不需要的時候用rmmod命令解除安裝模組,採用這種動態載入的方式便於驅動程式的除錯,同時可以針對產品的功能需求,進行核心的裁剪,將不需要的驅動去除,大大減小了核心的儲存容量。

在臺式機上,一般採用動態載入的方式;在嵌入式產品裡,可以先採用動態載入的方式進行除錯,除錯成功後再編譯進核心。

下面是一個最基礎的helloworld版的linux驅動,載入入核心後生成/dev/hello節點,開啟該檔案輸出” hello open”。這個驅動並不具有任何控制硬體的行為,只是為了展示linux驅動的通用結構。這幾乎是所有驅動程式的通用模版,如led的驅動程式,只需要在hello_ioctl函式中根據不同的傳入引數操作gpio暫存器即可。(應用層沒有操作硬體的許可權,而核心中具有所有許可權。驅動程式的作用就是高效的、封裝的、有限的嚮應用層提供服務)
#include <linux/init.h>
#include <linux/module.h>

#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>

#define DRIVER_NAME "hello"
#define DEVICE_NAME "hello"

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Radia");

static int hello_open(struct inode *inode, struct file *file){
	printk(KERN_EMERG "hello open\n");
	return 0;
}

static int hello_release(struct inode *inode, struct file *file){
	printk(KERN_EMERG "hello release\n");
	return 0;
}

static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
	printk("cmd is %d, arg is %d\n", cmd, arg);
	return 0;
}

static struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.open = hello_open,
	.release = hello_release,
	.unlocked_ioctl = hello_ioctl,
};

static struct miscdevice hello_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEVICE_NAME,
	.fops = &hello_fops,
};

static int hello_probe(struct platform_device *pdv)
{
	printk(KERN_EMERG "hello probe\n");
	misc_register(&hello_dev);
	return 0;
}

static int hello_remove(struct platform_device *pdv)
{
	printk(KERN_EMERG "hello remove\n");
	misc_deregister(&hello_dev);
	return 0;
}

static void hello_shutdown(struct platform_device *pdv)
{
}

static int hello_suspend(struct platform_device *pdv, pm_message_t pmt)
{
	return 0;
}

static int hello_resume(struct platform_device *pdv)
{
	return 0;
}

static struct platform_driver hello_driver = {
	.probe = hello_probe,
	.remove = hello_remove,
	.shutdown = hello_shutdown,
	.suspend = hello_suspend,
	.resume = hello_resume,
	.driver = {
		.name = DRIVER_NAME,
		.owner = THIS_MODULE,
	}
};

static int hello_init(void)
{
	int driver_state;
	printk(KERN_EMERG "hello module has been mount!\n");
	driver_state = platform_driver_register(&hello_driver);
	printk(KERN_EMERG "platform_driver_register driver_state is %d\n", driver_state);
	platform_device_register_simple(DRIVER_NAME, -1, NULL, 0);
	printk(KERN_EMERG "platform_device_register_simple end\n");
	return 0;
}

static void hello_exit(void)
{
	printk(KERN_EMERG "hello module has been remove!\n");
	platform_driver_unregister(&hello_driver);
}

module_init(hello_init);
module_exit(hello_exit);

程式分析:

1,  可以看出這個驅動模版也是基於我們上一個核心模組模版的。

2,  驅動模組一般在開機時逐次載入,載入成功後就會呼叫hello_init函式使用platform_driver_register向核心中註冊一個驅動。搞定,就是如此簡單!

3,  相關函式

字元裝置申請裝置號函式register_chrdev、register_chrdev_region

register_chrdev註冊指定主次裝置號的裝置,手動在/dev下建立該裝置的裝置節點

register_chrdev_region 註冊一個指定主次裝置號的裝置,後利用class類在/dev/目錄下自動建立一個該裝置的節點。

字元設備註冊函式cdev_add。

本例項為簡潔起見使用了misc_register函式,該函式已包含了上面兩個步驟。

字元裝置驅動註冊函式platform_driver_register。

4,  probe函式的呼叫

當裝置和驅動的名字匹配,BUS就會呼叫驅動的probe函式。這分兩種情況:

a 先註冊裝置,後註冊驅動

此種方式最為常見,大多數裝置先於驅動註冊到核心中。

在核心原始碼中,platform 裝置的初始化(註冊)用arch_initcall()呼叫,它的initcall 的level為3;而驅動的註冊用module_init()呼叫,即device_initcall(),它的initcall 的level為6。kernel 初始化時([email protected]/main.c),按照核心連結檔案中(arm系統:kernel/arch/arm/vmlinux.lds)的__initcall_start段的序列依次執行,這樣level小的初始化函式先於level大的初始化函式被呼叫。

所以platform裝置先被註冊,驅動載入時會呼叫驅動程式中的probe(),掃描系統中已註冊的裝置,找到匹配裝置後將驅動和裝置繫結。

當設備註冊的時候,由於驅動尚未註冊,所以執行"__device_attach"時直接返回,未執行"driver_probe_device"進行裝置和驅動匹配;後來驅動註冊的時候,執行"__driver_attach"的時候裝置已經註冊,所以進入"driver_probe_device",接著進入"driver_probe_device",然後"really_probe"完成裝置和驅動的繫結。

b 先註冊驅動,再註冊裝置

當驅動註冊的時候,由於裝置尚未註冊,所以執行"__driver_attach"時直接返回,未執行"driver_probe_device"進行驅動和裝置匹配;後來設備註冊的時候,執行"__device_attach"的時候驅動已經註冊,所以進入"driver_probe_device",接著進入"driver_probe_device",然後"really_probe"完成驅動和裝置的繫結。

本示例使用了最為簡潔明瞭的展示方法,在核心模組的載入函式hello_init中依次註冊了裝置與驅動。可知註冊後者時將會觸發hello_probe函式的呼叫,其中呼叫misc_register使用了主裝置號10,動態分配了次裝置號等,完成設備註冊。

5,  驗證

編譯驅動進核心,重新啟動在開機過程中看到hello模組成功掛載,probe函式呼叫。

在/dev目錄下生成了hello裝置節點,主裝置號10,次裝置號56。

編寫應用層程式進行測試:

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


int main(int argc, char *argv[])
{
	int fd;
	printf("enter driver test %s %s \r\n", argv[1], argv[2]);
	char *hello = "/dev/hello";

	if((fd = open(hello, O_RDWR | O_NOCTTY | O_NDELAY)) < 0)
	{
		printf("open %s failed\n", hello);
	}
	else
	{
		printf("%s fd is %d \r\n", hello, fd);
		ioctl(fd, atoi(argv[1]), atoi(argv[2]));
	}
	close(fd);
	return 1;
}

編譯後執行結果如下:

Shell 列印


Kernel log