基于ubuntu14.04 Linux内核驱动的编写

时间:2021-04-16 16:31:42
基于ubuntu14.04 Linux内核驱动的编写

    前面已经总结了关于安卓源码及Linux源码的编译,并且成功启动了模拟器,那么接下来就正式的编写安卓底层驱动了。在编写之前,我们应该先了解下Linux驱动编写的步骤。

    编写Linux驱动,主要是构建Linux驱动的框架,框架搭好了,整个驱动的编写工作也就完成一大半了。   

    Step 1:申请设备号(主要是申请主设备号)
           两种方式:(1)静态申请 函数

 int register_chrdev_region(dev_t from,unsigned count, const char *name);

/ * register_chrdev_region() - register arange of device numbers

 * @from: the first in the desired range of devicenumbers; must include

 *       the major number.

 * @count: the number of consecutive device numbersrequired

 * @name: the name of the device or driver.

 *Return value is zero on success, a negative error code on failure.*/

   这种方式主要用于,驱动开发者事先知道该驱动主设备号的情况

                      (2)动态申请

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,      const char *name)

/* alloc_chrdev_region() - register a rangeof char device numbers

 * @dev: output parameter for first assigned number

 * @baseminor: first of the requested range of minornumbers

 * @count: the number of minor numbers required

 * @name: the name of the associated device or driver

 *

 *Allocates a range of char device numbers. The major number will be

 *chosen dynamically, and returned (along with the first minor number)

 * in@dev.  Returns zero or a negative errorcode.*/

这种方式由系统动态分配一个设备号,返回的设备号保存在参数dev中。


    Step 2:注册字符设备

           在Linux内核中庸struct cdev表示一个字符设备。

           字符设备的注册与注销分别通过下面的两个函数来实现:

          

int cdev_add(structcdev *p, dev_t dev, unsigned count)

/**

 *cdev_add() - add a char device to the system

 *@p: the cdev structure for the device

 *@dev: the first device number for which this device is responsible

 *@count: the number of consecutive minor numbers corresponding to this

 *        device

 *

 *cdev_add() adds the device represented by @p to the system, making it

 *live immediately.  A negative error codeis returned on failure.

 */

void cdev_del(structcdev *p)

 

不过,在注册一个字符设备之前,要调用下面这个函数来初始化struct cdev结构体:

void cdev_init(structcdev *cdev, const struct file_operations *fops)

/**

 *cdev_init() - initialize a cdev structure

 *@cdev: the structure to initialize

 *@fops: the file_operations for this device

 *

 *Initializes @cdev, remembering @fops, making it ready to add to the

 *system with cdev_add().

 */

 

另外,struct cdev结构体变量可以声明为一个指针,内核提供了一个函数来申请:

struct cdev *cdev_alloc(void)


    Step 3:创建设备节点

          

有两种方法:

一是通过 mknod命令来创建。如:

  mknod /dev/yourname c major minor

其中yourname”可以是任意符合unix下路径名的名字,不一定要是你代码里定义的驱动或设备的名字;c 表示创建字符设备节点,major是你成功申请的主设备号,minor是次设备号,这个可以是任意的(在次设备号范围内)

 

另外一种方法是通过udev自动生成。这种方法需要在你的代码里创建一个设备类,然后在这个设备类的基础上,创建一个设备;另外应用程序需要跑一个udevd的后台程序。

struct class*  class_create(owner, name);

struct device *device_create(struct class *class, struct device *parent,

   dev_t devt, void *drvdata,const char *fmt, ...)


    这样Linux驱动编写的一般步骤就完成了,我们阅读Linux内核驱动的代码,应该先找到程序的入口函数module_init(char_test_init);参数中的就是函数入口,函数名可以自己定义,从入口进入,根据以上的步骤阅读代码,那么Linux内核驱动的框架就显得简单明了了。


    接下来以LED驱动为例子,阅读下LED驱动的代码。



#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h> 
#include <asm/string.h>
#include <asm/uaccess.h>//kmalloc函数头文
#include <linux/slab.h>
#include <linux/mutex.h>
//驱动头文件
#include <mach/gpio.h>        /*  \arch\arm\mach-s5pv210\include\  */
#include <mach/regs-gpio.h>
#include <plat/gpio-cfg.h>    /*  \arch\arm\plat-samsung\include\  */	

#define LED_ON _IOW('a',2,int)
#define LED_OFF _IOW('a',3,int)
#define LED_ALL_ON _IO('a',0xF)
#define LED_ALL_OFF _IO('a',0)
#define LED_LIUSHUI_ON _IO('a',98)
#define LED_LIUSHUI_OFF _IO('a',99)


MODULE_LICENSE("GPL");

int devno_major=0;
int devno_minor=0;

int init_gpio_led(void)
{
	if(!gpio_request(S5PV210_GPJ2(0), "led_1"))
	{
		return -1;
	}
	if(!gpio_request(S5PV210_GPJ2(1), "led_2"))
	{
		return -1;
	}
	if(!gpio_request(S5PV210_GPJ2(2), "led_3"))
	{
		return -1;
	}
	if(!gpio_request(S5PV210_GPJ2(3), "led_4"))
	{
		return -1;
	}
	gpio_direction_output(S5PV210_GPJ2(0), 1);
	gpio_direction_output(S5PV210_GPJ2(1), 1);
	gpio_direction_output(S5PV210_GPJ2(2), 1);
	gpio_direction_output(S5PV210_GPJ2(3), 1);
	
	
	return 0;
}


module_param(devno_major, int, 0440);

struct cdev *pdev=NULL;

struct class * myclass  = NULL; 
struct device *mdevice = NULL;


int test_open(struct inode *_inode,struct file *_file)
{
	printk(KERN_INFO "%s\n", __FUNCTION__);
	
	return 0;
}

int test_close(struct inode *_inode,struct file *_file)
{
	printk(KERN_INFO "%s\n", __FUNCTION__);
	return 0;
}

ssize_t  test_read (struct file *_file, char __user * buf, size_t count, loff_t * offset)
{
	return 0;
}
ssize_t  test_write (struct file *_file, const char __user * buf, size_t count, loff_t * offset)
{
	return 0;
}
	

long test_ioctl (struct file * _file, unsigned int cmd, unsigned long arg)
{
	int *args=(int *)arg;
	int k;
	if (_IOC_DIR(cmd) == _IOC_READ) //该命令,是用户想从内核读一个数据
	{
		//我就必须要验证你提供的地址,是否可写
		if (!access_ok(VERIFY_WRITE, arg, _IOC_SIZE(cmd)) )
		{
			return -EFAULT;
		}
	} 
	else if (_IOC_DIR(cmd) == _IOC_WRITE)
	{
		if (!access_ok(VERIFY_READ, arg, _IOC_SIZE(cmd)))
		{
			return -EFAULT;
		}
	}
	get_user(k,args);
	switch(cmd)
	{
		case LED_ON:
			__gpio_set_value(S5PV210_GPJ2(k),0);
			break;
		case LED_OFF:
			__gpio_set_value(S5PV210_GPJ2(k),1);
			break;
		case LED_ALL_ON:
			__gpio_set_value(S5PV210_GPJ2(0),0);
			__gpio_set_value(S5PV210_GPJ2(1),0);
			__gpio_set_value(S5PV210_GPJ2(2),0);
			__gpio_set_value(S5PV210_GPJ2(3),0);
			break;
		case LED_ALL_OFF:
			__gpio_set_value(S5PV210_GPJ2(0),1);
			__gpio_set_value(S5PV210_GPJ2(1),1);
			__gpio_set_value(S5PV210_GPJ2(2),1);
			__gpio_set_value(S5PV210_GPJ2(3),1);
			break;
		case LED_LIUSHUI_ON :
			__gpio_set_value(S5PV210_GPJ2(0),1);
			__gpio_set_value(S5PV210_GPJ2(0),0);
			__gpio_set_value(S5PV210_GPJ2(1),1);
			__gpio_set_value(S5PV210_GPJ2(1),0);
			__gpio_set_value(S5PV210_GPJ2(2),1);
			__gpio_set_value(S5PV210_GPJ2(2),0);
			__gpio_set_value(S5PV210_GPJ2(3),1);
			__gpio_set_value(S5PV210_GPJ2(3),0);
			break;
		case LED_LIUSHUI_OFF :
		
			break;
		default:
			break;
	}
	
	return -1;
}


const struct file_operations fops=        //传统的字符设备访问方式 这里我偷懒只用ioctl实现对硬件的访问
{
	.open =test_open,
	.release=test_close,
	.read=test_read,
	.write=test_write,
	.unlocked_ioctl = test_ioctl,
};


int  char_test_init(void)
{ 
	int r,res;
	dev_t devno;   //32位数,其中的12位用来表示主设备号,其余的20位表示次设备号
	
	//申请设备号
	if(devno_major>0)//静态指定
	{
		devno =MKDEV(devno_major,devno_minor);
		r=register_chrdev_region(devno,1,"test");
	}
	else//动态申请
	{
		r=alloc_chrdev_region(&devno, devno_minor, 1, "test");
	}
	if(r!=0)
	{
		printk(KERN_ERR "register char dev number failed\n");
		return -1;
	}
	devno_major=MAJOR(devno);    //获取主设备号
	devno_minor=MINOR(devno);    //获取次设备号
	printk(KERN_INFO"major: %d minor: %d\n", devno_major, devno_minor);

	//注册字符设备
	pdev =cdev_alloc();
	cdev_init(pdev,&fops);   //字符设备初始化
	cdev_add(pdev,devno,1);  //通过此函数告诉内核该结构的信息
	
	//生成设备节点
	myclass=class_create(THIS_MODULE, "char_test");
	mdevice=device_create(myclass,NULL,devno,NULL,"test");
	
	res=init_gpio_led();//初始化端口
	if(res!=0)
		return -1;
	return 0;
}

void char_test_exit(void)
{
	dev_t devno=MKDEV(devno_majkfree(pt->p_buf);
	//释放设备号
	
	device_destroy(myclass,devno);
	class_destroy(myclass);
	
	
	//注销字符设备
	cdev_del(pdev);
	gpio_free(S5PV210_GPJ2(0));	
	gpio_free(S5PV210_GPJ2(1));	
	gpio_free(S5PV210_GPJ2(2));	
	gpio_free(S5PV210_GPJ2(3));	
	unregister_chrdev_region(devno,1);	
}

module_init(char_test_init);  //这里为函数入口
module_exit(char_test_exit);  //这里为退出函数


    理解了上述代码后,那么如何将自己编写的驱动写进Linux内核呢?

    我们在linux内核中编写驱动,一般都是在/kernel/driver/ 下建立自己的目录,在新建的目录下创建C文件编写。比如说新建hello目录,在hello目录下新建hello.c及hello.h一些相关的头文件。驱动编写完成后,还需要配置Kconfig以及Makefile文件。以hello为列:

    其中Kconfig是在编译前执行配置命令make menuconfig时用到的,而Makefile是执行编译命令make是用到的:

    Kconfig文件的内容

       config HELLO

           tristate "First Android Driver"

           default n

           help

           This is the first android driver.


    

    Makefile文件的内容

      obj-$(CONFIG_HELLO) += hello.o

    

    在Kconfig文件中,tristate表示编译选项HELLO支持在编译内核时,hello模块支持以模块、内建和不编译三种编译方法,默认是不编译,因此,在编译内核前,我们还需要执行make menuconfig命令来配置编译选项,使得hello可以以模块或者内建的方法进行编译。

      在Makefile文件中,根据选项HELLO的值,执行不同的编译方法

      修改arch/arm/Kconfig和drivers/kconfig两个文件,在menu "Device Drivers"和endmenu之间添加一行:

      source "drivers/hello/Kconfig"(有些源码在arch/arm/Kconfig中没有menu "Device Drivers"和endmenu,那是因为在drivers/kconfig已经包含。所以只需要在drivers/kconfig添加即可。)

        这样,执行make menuconfig时,就可以配置hello模块的编译选项了。 

        

        修改drivers/Makefile文件,添加一行:

        obj-$(CONFIG_HELLO) += hello/

        配置编译选项:

        /Android-5.0.2/kernel/common$ make menuconfig

        找到"Device Drivers" => "First Android Drivers"选项,设置为y

    注意,如果内核不支持动态加载模块,这里不能选择m,虽然我们在Kconfig文件中配置了HELLO选项为tristate。要支持动态加载模块选项,必须要在配置菜单中选择Enable loadable module support选项;在支持动态卸载模块选项,必须要在Enable loadable module support菜单项中,选择Module unloading选项。 


     这些工作做完之后,就可以直接在/kernel目录下make了。

     编译成功后,就可以在hello目录下看到hello.o文件了,这时候编译出来的zImage已经包含了hello驱动。


参考博客:http://blog.csdn.net/luoshengyang/article/details/6568411 罗老师