字符设备驱动

第001节_字符设备驱动程序之概念介绍

知识梳理

  • uboot的目的是启动内核。
  • 内核的目的是启动应用。
  • 应用程序涉及到很多文件操作、硬件操作(点灯、获取按键值等)。对于写应用程序的人不应该涉及到硬件操作。在应用层有:open、read、write等操作,在驱动层也会有相应的驱动函数。

举例说明:

int main()
{
	int fd1,fd2;
	fd1 = open("/dev/led",O_RDWR)write(fd1,&val,4);
	
	fd1 = open("hello.txt",O_RDWR)write(fd2,&val,4);
}

C库实现了应用层的open、read、write,当我们的应用程序调用上面的接口的时候会进入内核。open、read、write会触发异常(swi指令 swi val),这条汇编指令会引发一个异常,会进入异常处理函数(system call interface:系统调用接口),系统调用接口的任务是:在异常处理函数里面根据发生中断的原因调用不同的处理函数。比如说,用open的时候传入的值是Val1、用read的时候传入的值是Val2、用write的时候传入的值是Val3,系统调用接口会根据传入的不同的值调用我们的接口:sys_open、sys_read、sys_write(VFS层,虚拟文件系统)。

为了实现同一个函数实现不同的行为(比如同一个write函数可以实现LED点灯、FLASH写),sys_open、sys_read、sys_write会根据打开的不同文件找到不同的底层驱动程序,调用不同驱动程序的drv_open、drv_read、drv_write函数。

具体框架如下两图所示:
在这里插入图片描述

在这里插入图片描述

第002节_字符设备驱动程序之LED驱动程序_编写编译

本节以实例进行讲解。

如何把驱动程序告诉内核去调用?

创建一个应用程序:First_drv.c,并添加如下内容:

static int first_drv_open(struct inode *inode,struct file *file)
{
	return 0;
}

static int first_drv_write(struct file *file,const char __user *buf,size_t count.loff_t *ppos)
{
	return 0;
}

我们写完程序之后怎么告诉内核呢?首先我们为什么要告诉内核呢?
我们的程序是从上到下去调用的,经过了内核层,那我们肯定要告诉内核。那么如何告诉内核呢?我们需要定义如下结构体并填充它:

static struct file_operations first_drv_fops = {
	.owner = THIS_MODULE,
	.open = first_drv_open,
	.write = first_drv_write,

我们还需要把这个结构告诉内核,添加如下函数:

int first_drv_init(void)//入口函数
{
	register_chrdev(major,"first_drv",&first_drv_fops );//注册驱动程序,告诉内核
	return 0;
}

但上述函数也要被调用,因此我们需要修饰此入口函数,用module_init();来修饰,如下所示:

module_init(first_drv_init);

这个module_init定义了一个结构体,里面有一个函数指针,指向first_drv_init,当我们安装一个驱动程序的时候,内核就要自动去找到这么一个结构体,然后去调用这一个函数指针。

具体的调用原理

我们在Ubuntu中看看我们的驱动设备:ls /dev -l,可以看到下图的信息:
在这里插入图片描述

第一个字符(红色框)表示类型:

  • c表示字符设备
  • -表示常规设备
  • d表示目录

蓝色框表示主设备号,黄色框表示次设备号。

在我们的应用程序中打开一个设备文件:

open("/dev/xxx")

那么这个xxx有什么属性呢?

c(字符设备)_ _ _ _ _ _ _ _ _(_表示读写执行权限)     major(主设备号)  mior(次设备号)

那么最终怎么调用到我们的file_operations的成员呢?我们猜想一下有以下可能:

  • 通过名字进行调用。
  • 通过设备类型和主设备号。

事实告诉我们不是第一种,我们的VFS系统就是根据第2种方法通过打开文件,打开了这个文件里的属性,它是属于字符设备,然后把主设备号提取出来,根据这两个属性就能找到我们注册进去的file_operations 结构,我们想象一下,这个是怎么实现的?

在内核数组里面(chrdev),里面都是主设备号,然后把file_operations结构填充进去。

0123major

完善驱动程序

我们既然有入口函数,我们注册时把file_operations 挂到我们的VFS中的主设备号上去了,那如果我们不想使用了,想要卸载了要怎么做呢?

我们需要再定义一个出口函数:

void first_drv_exit(void)
{
	unregister_chrdev(111,"first_drv");//注册驱动程序,告诉内核
}

显然我们也需要修饰它:

modules_exit(first_drv_exit);

我们修改一个open和write,打印一些信息:

static int first_drv_open(struct inode *inode,struct file *file)
{
	printk("first_drv_open\n");
	return 0;
}

static int first_drv_write(struct file *file,const char __user *buf,size_t count.loff_t *ppos)
{
	printk("first_drv_write\n");
	return 0;
}

我们还需要加入一些头文件:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

编写完后我们就可以放入Ubuntu中去编译了,我们创建文件夹并把我们的first_drv.c放入进去:

mkdir /work/drivers_and_test/first_drv

编译

我们还需要在上面添加Makefile文件内容如下:

KERN_DIR = /work/system/linux-2.6.22.6

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= first_drv.o

之后执行make指令就可以生成first_drv.ko可执行文件了。

第003节_字符设备驱动程序之LED驱动程序_测试改进

查看当前支持设备

我们在上一节生成了first_drv.ko程序,我们在本节要运行并改善它。
加载之前我们先来看一下内核现在支持的设备:cat /proc/devices
在这里插入图片描述
如上图所示,红色框为字符设备、蓝色框为块设备,第一列为主设备号、第二列为名字。

加载设备

使用:insmod first_drv.ko命令加载驱动。

加载后我们在看一下内核支持的设备程序
在这里插入图片描述
可以看到我们的程序已经被加载进来了。
我们现在还需要写一个测试程序来测试它。

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

int main(int argc, char **argv)
{
	int fd;
	int val = 1;
	fd = open("/dev/xxx", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}
	write(fd, &val, 4);
	return 0;
}

之后编译并copy到我们的那个目录上去:

arm-linux-gcc -o firstdrvtest firstdrvtest.c
cp fitstdrvtest /work/nfs/first_fs

然后在单板上执行这个程序:

./firstdrvtest

发现并不能开发,打印can't open!,那么为什么不能打开呢?显然是不存在/dev/xxx这个文件。

我们来创建这个设备节点:

//字符设备 主设备号为111 次设备号为0
mknod /dev/xxx c 111 0

这时我们再执行测试程序,此时就可以看到有如下输出:

first_drv_open
first_drv_write

上面的程序是我们自己分配主设备号的,我们可以通过更改first_drv_init和 来自动分配。

int major ;
static int first_drv_init(void)//入口函数
{
	major = register_chrdev(0,"first_drv",&first_drv_fops );//注册驱动程序,告诉内核,返回值major就是主设备号。
	return 0;
}

我们还需要让程序能自动设备节点(udev和mdev)。
我们注册一个驱动程序的时候会在系统(单板)的sys目录下生成设备的信息。而我们的mdev会根据这些系统信息自动的创建设备节点。

首先定义下面两个变量:

static struct class *firstdrv_class;
static struct class_device	*firstdrv_class_dev;

更改入口函数,生成系统信息,具体过程为:下面两条会创建firstdrv这个类,这个类下面会创建xyz这一个设备,然后我们的mdev就会自动的创建/dev/xyz这一个设备节点。

int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

	firstdrv_class = class_create(THIS_MODULE, "firstdrv");//先创建一个类

	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

	return 0;
}

更改出口函数,删掉这些变量:

static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载

	class_device_unregister(firstdrv_class_dev);
	class_destroy(firstdrv_class);
	iounmap(gpfcon);
}

我们重新装载一下驱动后:

rmmod first_drv
insmod ./first_drv.ko

第004节_字符设备驱动程序之LED驱动程序_操作LED

写一个点LED驱动步骤:

  1. 框架(已搭好)
  2. 看原理图
  3. 看2440手册
  4. 写驱动程序(虚拟地址,用ioremap映射)

配置GPFCON(物理地址:0x56000000)寄存器设置为输出引脚(open函数中写),设置GPFDAT输出数据(write函数中写)。

首先定义两个全局变量:

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;

接着在入口函数中添加虚拟化映射:

int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

	firstdrv_class = class_create(THIS_MODULE, "firstdrv");

	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;
	return 0;
}

出口函数:

static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv"); // 卸载

	class_device_unregister(firstdrv_class_dev);
	class_destroy(firstdrv_class);
	iounmap(gpfcon);
}

open函数:

static int first_drv_open(struct inode *inode, struct file *file)
{
	//printk("first_drv_open\n");
	/* 配置GPF4,5,6为输出 */
	*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
	*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
	return 0;
}

write函数:

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int val;
	copy_from_user(&val, buf, count); //用此函数传入用户程序的数据  copy_to_user传递数据到用户空间
	if (val == 1)
	{
		// 点灯
		*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
	}
	else
	{
		// 灭灯 
		*gpfdat |= (1<<4) | (1<<5) | (1<<6);
	}
	
	return 0;
}

修改驱动测试程序:

int main(int argc, char **argv)
{
	int fd;
	int val = 1;
	fd = open("/dev/xyz", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}
	if (argc != 2)//参数的个数,不等于2就退出
	{
		printf("Usage :\n");
		printf("%s <on|off>\n", argv[0]);//./firstdrvtest on/off
		return 0;
	}

	if (strcmp(argv[1], "on") == 0)//第二个参数
	{
		val  = 1;
	}
	else
	{
		val = 0;
	}
	
	write(fd, &val, 4);
	return 0;
}

运行后通过命令:./firstdrvtest on/off就可以进行硬件操作了。

通过上面的程序我们可以知道与单片机写驱动程序的差别了:

  • 单片机操作的寄存器是物理地址,而linux中不能直接使用物理地址,而是虚拟地址,虚拟地址通过ioremap映射得到。

支持单个灯点亮操作

完整的驱动程序:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

#define DEVICE_NAME     "leds"  /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
#define LED_MAJOR       231     /* 主设备号 */


static struct class *leds_class;
static struct class_device	*leds_class_devs[4];


/* bit0<=>D10, 0:亮, 1:灭 
 *  bit1<=>D11, 0:亮, 1:灭 
 *  bit2<=>D12, 0:亮, 1:灭 
 */ 
static char leds_status = 0x0;  
static DECLARE_MUTEX(leds_lock); // 定义赋值

//static int minor;
static unsigned long gpio_va;

#define GPIO_OFT(x) ((x) - 0x56000000)
#define GPFCON  (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000050)))
#define GPFDAT  (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000054)))


/* 应用程序对设备文件/dev/leds执行open(...)时,
 * 就会调用s3c24xx_leds_open函数
 */
static int s3c24xx_leds_open(struct inode *inode, struct file *file)
{
	int minor = MINOR(inode->i_rdev); //MINOR(inode->i_cdev);

	switch(minor)
	{
        case 0: /* /dev/leds */
        {
            // 配置3引脚为输出
            //s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP);
            GPFCON &= ~(0x3<<(4*2));
            GPFCON |= (1<<(4*2));
            
            //s3c2410_gpio_cfgpin(S3C2410_GPF5, S3C2410_GPF5_OUTP);
            GPFCON &= ~(0x3<<(5*2));
            GPFCON |= (1<<(5*2));

            //s3c2410_gpio_cfgpin(S3C2410_GPF6, S3C2410_GPF6_OUTP);
            GPFCON &= ~(0x3<<(6*2));
            GPFCON |= (1<<(6*2));

            // 都输出0
            //s3c2410_gpio_setpin(S3C2410_GPF4, 0);
            GPFDAT &= ~(1<<4);
            
            //s3c2410_gpio_setpin(S3C2410_GPF5, 0);
            GPFDAT &= ~(1<<5);
            //s3c2410_gpio_setpin(S3C2410_GPF6, 0);
            GPFDAT &= ~(1<<6);

            down(&leds_lock);
            leds_status = 0x0;
            up(&leds_lock);
                
            break;
        }

        case 1: /* /dev/led1 */
        {
            s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP);
            s3c2410_gpio_setpin(S3C2410_GPF4, 0);
            
            down(&leds_lock);
            leds_status &= ~(1<<0);
            up(&leds_lock);
            
            break;
        }

        case 2: /* /dev/led2 */
        {
            s3c2410_gpio_cfgpin(S3C2410_GPF5, S3C2410_GPF5_OUTP);
            s3c2410_gpio_setpin(S3C2410_GPF5, 0);
            leds_status &= ~(1<<1);
            break;
        }

        case 3: /* /dev/led3 */
        {
            s3c2410_gpio_cfgpin(S3C2410_GPF6, S3C2410_GPF6_OUTP);
            s3c2410_gpio_setpin(S3C2410_GPF6, 0);

            down(&leds_lock);
            leds_status &= ~(1<<2);
            up(&leds_lock);
            
            break;
        }
        
	}
	
    return 0;
}



static int s3c24xx_leds_read(struct file *filp, char __user *buff, 
                                         size_t count, loff_t *offp)
{
	int minor = MINOR(filp->f_dentry->d_inode->i_rdev);
    char val;

    switch (minor)
    {
        case 0: /* /dev/leds */
        {
            
            copy_to_user(buff, (const void *)&leds_status, 1);                    
            break;
        }

        case 1: /* /dev/led1 */
        {
            down(&leds_lock);
            val = leds_status & 0x1;
            up(&leds_lock);
            copy_to_user(buff, (const void *)&val, 1);
            break;
        }

        case 2: /* /dev/led2 */
        {
            down(&leds_lock);
            val = (leds_status>>1) & 0x1;
            up(&leds_lock);
            copy_to_user(buff, (const void *)&val, 1);
            break;
        }

        case 3: /* /dev/led3 */
        {
            down(&leds_lock);
            val = (leds_status>>2) & 0x1;
            up(&leds_lock);
            copy_to_user(buff, (const void *)&val, 1);
            break;
        }
        
    }

    return 1;
}




static ssize_t s3c24xx_leds_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    //int minor = MINOR(inode->i_rdev); //MINOR(inode->i_cdev);
	int minor = MINOR(file->f_dentry->d_inode->i_rdev);
    char val;

    copy_from_user(&val, buf, 1);

    switch (minor)
    {
        case 0: /* /dev/leds */
        {            
            s3c2410_gpio_setpin(S3C2410_GPF4, (val & 0x1));
            s3c2410_gpio_setpin(S3C2410_GPF5, (val & 0x1));
            s3c2410_gpio_setpin(S3C2410_GPF6, (val & 0x1));
            down(&leds_lock);
            leds_status = val;
            up(&leds_lock);
            break;
        }

        case 1: /* /dev/led1 */
        {
            s3c2410_gpio_setpin(S3C2410_GPF4, val);

            if (val == 0)
            {
                down(&leds_lock);
                leds_status &= ~(1<<0);
                up(&leds_lock);
            }
            else
            {
                down(&leds_lock);
                leds_status |= (1<<0);                
                up(&leds_lock);
            }
            break;
        }

        case 2: /* /dev/led2 */
        {
            s3c2410_gpio_setpin(S3C2410_GPF5, val);
            if (val == 0)
            {
                down(&leds_lock);
                leds_status &= ~(1<<1);
                up(&leds_lock);
            }
            else
            {
                down(&leds_lock);
                leds_status |= (1<<1);                
                up(&leds_lock);
            }
            break;
        }

        case 3: /* /dev/led3 */
        {
            s3c2410_gpio_setpin(S3C2410_GPF6, val);
            if (val == 0)
            {
                down(&leds_lock);
                leds_status &= ~(1<<2);
                up(&leds_lock);
            }
            else
            {
                down(&leds_lock);
                leds_status |= (1<<2);                
                up(&leds_lock);
            }
            break;
        }
        
    }

    return 1;
}



/* 这个结构是字符设备驱动程序的核心
 * 当应用程序操作设备文件时所调用的open、read、write等函数,
 * 最终会调用这个结构中指定的对应函数
 */
static struct file_operations s3c24xx_leds_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   s3c24xx_leds_open,     
	.read	=	s3c24xx_leds_read,	   
	.write	=	s3c24xx_leds_write,	   
};

/*
 * 执行insmod命令时就会调用这个函数 
 */
static int __init s3c24xx_leds_init(void)
//static int __init init_module(void)

{
    int ret;
	int minor = 0;

    gpio_va = ioremap(0x56000000, 0x100000);
	if (!gpio_va) {
		return -EIO;
	}

    /* 注册字符设备
     * 参数为主设备号、设备名字、file_operations结构;
     * 这样,主设备号就和具体的file_operations结构联系起来了,
     * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
     * LED_MAJOR可以设为0,表示由内核自动分配主设备号
     */
    ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);
    if (ret < 0) {
      printk(DEVICE_NAME " can't register major number\n");
      return ret;
    }

	leds_class = class_create(THIS_MODULE, "leds");
	if (IS_ERR(leds_class))
		return PTR_ERR(leds_class);
    


	leds_class_devs[0] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "leds"); /* /dev/leds */
	
	for (minor = 1; minor < 4; minor++)  /* /dev/led1,2,3 */
	{
		leds_class_devs[minor] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, minor), NULL, "led%d", minor);
		if (unlikely(IS_ERR(leds_class_devs[minor])))
			return PTR_ERR(leds_class_devs[minor]);
	}
        
    printk(DEVICE_NAME " initialized\n");
    return 0;
}

/*
 * 执行rmmod命令时就会调用这个函数 
 */
static void __exit s3c24xx_leds_exit(void)
{
	int minor;
    /* 卸载驱动程序 */
    unregister_chrdev(LED_MAJOR, DEVICE_NAME);

	for (minor = 0; minor < 4; minor++)
	{
		class_device_unregister(leds_class_devs[minor]);
	}
	class_destroy(leds_class);
    iounmap(gpio_va);
}

/* 这两行指定驱动程序的初始化函数和卸载函数 */
module_init(s3c24xx_leds_init);
module_exit(s3c24xx_leds_exit);

/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("http://www.100ask.net");
MODULE_VERSION("0.1.0");
MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver");
MODULE_LICENSE("GPL");

小结

  • 执行insmod命令时就会调用这个s3c24xx_leds_init函数
  • s3c24xx_leds_init函数:注册字符设备,联系主设备号和file_operations结构体、生成系统信息创建设备节点,引脚映射。
  • file_operations保存了open、close、read、write函数
  • open函数里面配置引脚初始化
  • write:写入值
  • read:返回值
  • insmod ./first_drv.ko–挂载
  • rmmod first_drv – 卸载
  • mknod /dev/xxx c 111 0–创建设备节点(文件名、主设备号、次设备号)
  • cat /proc/devices:查看当前设备

第005节_字符设备驱动程序之查询方式的按键驱动程序

代码与上节类似:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *seconddrv_class;
static struct class_device	*seconddrv_class_dev;

volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;

volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;

static int second_drv_open(struct inode *inode, struct file *file)
{
	/* 配置GPF0,2为输入引脚 */
	*gpfcon &= ~((0x3<<(0*2)) | (0x3<<(2*2)));

	/* 配置GPG3,11为输入引脚 */
	*gpgcon &= ~((0x3<<(3*2)) | (0x3<<(11*2)));

	return 0;
}

ssize_t second_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	/* 返回4个引脚的电平 */
	unsigned char key_vals[4];
	int regval;

	if (size != sizeof(key_vals))
		return -EINVAL;

	/* 读GPF0,2 */
	regval = *gpfdat;
	key_vals[0] = (regval & (1<<0)) ? 1 : 0;
	key_vals[1] = (regval & (1<<2)) ? 1 : 0;
	

	/* 读GPG3,11 */
	regval = *gpgdat;
	key_vals[2] = (regval & (1<<3)) ? 1 : 0;
	key_vals[3] = (regval & (1<<11)) ? 1 : 0;

	copy_to_user(buf, key_vals, sizeof(key_vals));
	
	return sizeof(key_vals);
}


static struct file_operations sencod_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   second_drv_open,     
	.read	=	second_drv_read,	   
};


int major;
static int second_drv_init(void)
{
	major = register_chrdev(0, "second_drv", &sencod_drv_fops);

	seconddrv_class = class_create(THIS_MODULE, "second_drv");

	seconddrv_class_dev = class_device_create(seconddrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */

	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;

	gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
	gpgdat = gpgcon + 1;

	return 0;
}

static void second_drv_exit(void)
{
	unregister_chrdev(major, "second_drv");
	class_device_unregister(seconddrv_class_dev);
	class_destroy(seconddrv_class);
	iounmap(gpfcon);
	iounmap(gpgcon);
	return 0;
}


module_init(second_drv_init);

module_exit(second_drv_exit);

MODULE_LICENSE("GPL");

第006节_字符设备驱动程序之中断方式的按键驱动_Linux异常处理结构_1

之前的裸机中断处理流程

  1. 按键按下
  2. CPU发生中断,跳到异常向量入口执行
  3. b 某函数

函数里面的内容:
1.保存被中断的现场
2.执行中断处理函数
3.恢复

linux下的中断处理流程

对于arm架构来说CPU的异常向量地址可以是0x00000000,也可以是0xFFFF0000。我们的内核使用过的是0xFFFF0000。trap_init函数将异常向量复制到0xFFFF0000处。

流程:
1.trap_init函数构造异常向量
2.调用恢复。

第007节_字符设备驱动程序之中断方式的按键驱动_Linux异常处理结构_2

单片机下的中断处理:

  • 分辨是哪一个中断
  • 调用处理函数
  • 清中断

单片机中的三个处理流程都是用arm_do_IRQ这个函数实现的。
linux内核下:
按下按键后

  1. 进入异常模式 b vector_irq + ____
  2. __irq_usr
  3. b asm_do_IRQ
  4. irq_desc[irq]->handle_irq
  5. handle_edge_irq
    a.desc->chip->ack(irq):清中断
    b.handle_IRQ_event:处理中断
    ①.取出action链表中的成员
    ②.执行:action->handler

action->handler是我们自己的代码,我们用request.irq告诉内核我们的处理函数。

总结图如下:
在这里插入图片描述

注册与卸载中断程序

注册中断程序

request_irq(irq,handler,flags,name,dev_id)
参数:

  • irq:中断号
  • handler:中断处理函数
  • flags:上升沿触发还是下降沿触发等
  • name:名字
  • dev_id:id与卸载时有关

功能:创建分配一个irq_action结构,把这个结构放入irq_desc[irq]数组里面的action链表里面。然后设置引脚(使能中断)、设置中断处理函数。

卸载中断程序

free_irq(irq,dev_id)
功能:出链,禁止中断

第008节_字符设备驱动程序之中断方式的按键驱动_编写代码

修改open函数如下:

static int third_drv_open(struct inode *inode, struct file *file)
{
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", 1);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3",1);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", 1);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", 1);	

	return 0;
}

添加release函数

static struct file_operations sencod_drv_fops = {
    .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open    =  third_drv_open,     
	.read	 =	third_drv_read,	   
	.release =  third_drv_close,	   
};
int third_drv_close(struct inode *inode, struct file *file)
{
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	return 0;
}

挂载驱动后我们我们用cat /proc/interrupts命令去查看一下中断的设备,结果如下:
在这里插入图片描述
上面是没有我们的设备的,现在我们用exec 5<dev/buttons打开设备S2、S3、S4、S5设备,再次查看可以看到上述设备已经被打开了:
在这里插入图片描述
上面exec中的5是:打开这一个设备,把他定位到5去。
我们通过ps看一下当前进程:
在这里插入图片描述
可以看到当前进程为-sh(771),我们通过命令:

ls -l /proc/771/fd

可以看到下列结果:
在这里插入图片描述
5就执向了 /dev/buttons
想关闭的话用下列指令:exec 5<&-

最终的按键程序如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>


static struct class *thirddrv_class;
static struct class_device	*thirddrv_class_dev;

volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;

volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;


static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

/* 中断事件标志, 中断服务程序将它置1,third_drv_read将它清0 */
static volatile int ev_press = 0;


struct pin_desc{
	unsigned int pin;
	unsigned int key_val;
};


/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */
static unsigned char key_val;

struct pin_desc pins_desc[4] = {
	{S3C2410_GPF0, 0x01},
	{S3C2410_GPF2, 0x02},
	{S3C2410_GPG3, 0x03},
	{S3C2410_GPG11, 0x04},
};


/*
  * 确定按键值
  */
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
	struct pin_desc * pindesc = (struct pin_desc *)dev_id;
	unsigned int pinval;
	
	pinval = s3c2410_gpio_getpin(pindesc->pin);

	if (pinval)
	{
		/* 松开 */
		key_val = 0x80 | pindesc->key_val;
	}
	else
	{
		/* 按下 */
		key_val = pindesc->key_val;
	}

    ev_press = 1;                  /* 表示中断发生了 */
    wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */

	
	return IRQ_RETVAL(IRQ_HANDLED);
}

static int third_drv_open(struct inode *inode, struct file *file)
{
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}

ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	if (size != 1)
		return -EINVAL;

	/* 如果没有按键动作, 休眠 */
	wait_event_interruptible(button_waitq, ev_press);

	/* 如果有按键动作, 返回键值 */
	copy_to_user(buf, &key_val, 1);
	ev_press = 0;
	
	return 1;
}


int third_drv_close(struct inode *inode, struct file *file)
{
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	return 0;
}


static struct file_operations sencod_drv_fops = {
    .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open    =  third_drv_open,     
	.read	 =	third_drv_read,	   
	.release =  third_drv_close,	   
};


int major;
static int third_drv_init(void)
{
	major = register_chrdev(0, "third_drv", &sencod_drv_fops);

	thirddrv_class = class_create(THIS_MODULE, "third_drv");

	thirddrv_class_dev = class_device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */

	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;

	gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
	gpgdat = gpgcon + 1;

	return 0;
}

static void third_drv_exit(void)
{
	unregister_chrdev(major, "third_drv");
	class_device_unregister(thirddrv_class_dev);
	class_destroy(thirddrv_class);
	iounmap(gpfcon);
	iounmap(gpgcon);
	return 0;
}

module_init(third_drv_init);
module_exit(third_drv_exit);
MODULE_LICENSE("GPL");

第009节_字符设备驱动程序之poll机制

分析

当系统调用poll的时候就会调用内核的sys_poll。sys_poll的调用层级如下:

app:poll
kernel:sys_poll()
		 do_sys_poll(...,timeout_jiffies)
			poll_initwait(&table)//初始化函数
				init_poll_funcptr(&pwq->pt,__pollwait); ->pt->qproc = qproc
			do_poll(nfds,head,&table,timeout)
			for(;;)//死循环
			{
				if(do_poll(pfd,pt)){ ->mask = file->f_op->poll(file,pwait); return mask;
									//驱动的poll:
									__pollwait(filp,&button_waitq,p);//把当前进程挂到button_waitq队列里去
									
									
					count++;//如果驱动的poll返回非0值,那么count++
					pt=NULL;
				}
				//break的条件:count非0,超时、有信号在等待处理
				if(count || !*timeout ||signal_pending(current))
				 	break;
				//休眠__timeout时间
				timeout = schedule_timeout(__timeout);
			}

实操

我们的应用程序有:open、read、write等,那么在驱动程序中也有相应的drv_open、drv_read、drv_write。同样的在应用程序中有poll的话,在驱动程序中应该也得有drv_poll

我们创建一个函数:

static unsigned forth_drv_poll(struct file *file, poll_table *wait)
{
	unsigned int mask = 0;
	poll_wait(file, &button_waitq, wait); // 不会立即休眠

	if (ev_press)
		mask |= POLLIN | POLLRDNORM;

	return mask;
}

并且放入结构体中:

static struct file_operations sencod_drv_fops = {
    .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open    =  forth_drv_open,     
	.read	 =	forth_drv_read,	   
	.release =  forth_drv_close,
	.poll    =  forth_drv_poll,
};

测试程序:


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


/* forthdrvtest 
  */
int main(int argc, char **argv)
{
	int fd;
	unsigned char key_val;
	int ret;

	struct pollfd fds[1];
	
	fd = open("/dev/buttons", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}

	fds[0].fd     = fd;
	fds[0].events = POLLIN;
	while (1)
	{
		ret = poll(fds, 1, 5000);
		if (ret == 0)
		{
			printf("time out\n");
		}
		else
		{
			read(fd, &key_val, 1);
			printf("key_val = 0x%x\n", key_val);
		}
	}
	
	return 0;
}


第010节_字符设备驱动程序之异步通知

我们之前的几种查询按键的方式:

  • 查询:耗资源
  • 中断:一直read()
  • poll:可以指定超时时间

共同点:都是应用程序主动去查询

有没有一个方法让驱动程序去提醒应用?

我们先写一个进程之间发信号的程序:

#include <stdio.h>
#include <poll.h>
#include <signal.h>


/* fifthdrvtest 
  */
int fd;

void my_signal_fun(int signum)
{
	static int cnt = 0;
	printf("signal: %d, %d times\n", signum,cnt);
}

int main(int argc, char **argv)
{
	signal(SIGUSR1, my_signal_fun);

	while (1)
	{
		sleep(1000);
	}
	
	return 0;
}

我们首先通过ps命令打印出进程号,可以看到我们的进程号为833:
在这里插入图片描述
然后我们通过kill -USR1 833发送信号:
在这里插入图片描述

记录一下信号的要点:

  • 注册一个信号处理函数
  • 谁发?
  • 发给谁?
  • 怎么发?

我们的目标:按下按键时,驱动通知应用程序。
步骤:

  • 应用程序需要注册信号处理函数。
  • 驱动程序发。
  • 发给应用程序,所以应用程序要告诉驱动程序PID。
  • 怎么发?驱动程序调用某个函数发送。(kill_fasync)

我们先定义一个结构:

static struct fasync_struct *button_async;

修改按键中断函数:

/*
  * 确定按键值
  */
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
	struct pin_desc * pindesc = (struct pin_desc *)dev_id;
	unsigned int pinval;
	
	pinval = s3c2410_gpio_getpin(pindesc->pin);

	if (pinval)
	{
		/* 松开 */
		key_val = 0x80 | pindesc->key_val;
	}
	else
	{
		/* 按下 */
		key_val = pindesc->key_val;
	}

    ev_press = 1;                  /* 表示中断发生了 */
    wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */
	
	kill_fasync (&button_async, SIGIO, POLL_IN);//发送信号
	
	return IRQ_RETVAL(IRQ_HANDLED);
}

创建fifth_drv_fasync 函数:

static int fifth_drv_fasync (int fd, struct file *filp, int on)
{
	printk("driver: fifth_drv_fasync\n");
	return fasync_helper (fd, filp, on, &button_async);
} 

修改file_operations结构体:

static struct file_operations sencod_drv_fops = {
    .owner   =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open    =  fifth_drv_open,     
	.read	 =	fifth_drv_read,	   
	.release =  fifth_drv_close,
	.poll    =  fifth_drv_poll,
	.fasync	 =  fifth_drv_fasync,
};

修改应用程序:


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


/* fifthdrvtest 
  */
int fd;

void my_signal_fun(int signum)
{
	unsigned char key_val;
	read(fd, &key_val, 1);
	printf("key_val: 0x%x\n", key_val);
}

int main(int argc, char **argv)
{
	unsigned char key_val;
	int ret;
	int Oflags;

	signal(SIGIO, my_signal_fun);//注册信号处理函数
	
	fd = open("/dev/buttons", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}

	fcntl(fd, F_SETOWN, getpid());//告诉驱动程序PID
	
	Oflags = fcntl(fd, F_GETFL); 
	
	fcntl(fd, F_SETFL, Oflags | FASYNC);//异步通知,这一步之后,驱动的.fasync就会被调用


	while (1)
	{
		sleep(1000);
	}
	
	return 0;
}



上面的中断处理函数会发送信号给应用程序,应用程序的信号处理函数:my_signal_fun就会被调用,即可通知应用程序。

第011节_字符设备驱动程序之同步互斥阻塞

目的:同一时刻,只能有一个应用程序打开/dev/buttion

我们写一个测试程序:
定义一个变量

static int canopen = 1;

然后修改open函数:

static int sixth_drv_open(struct inode *inode, struct file *file)
{
	if((--canopen)!=0)
	{
		canopen++;
		return -EBUSY;
	}
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}

修改close函数:

int sixth_drv_close(struct inode *inode, struct file *file)
{
	canopen++;
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	return 0;
}

因为在--canopen的时候会被分为三步:读取、修改、写会。这样会有可能导致一个驱动程序被多个应用程序OPEN。如A程序执行到读取的时候,被切换到另外一个B程序,这样B程序也会执行open操作,这样最后结果是A程序和B程序都打开了驱动。

在这里插入图片描述

原子操作

原子操作指的是在执行过程中不会被别的代码路径所中断的操作。

函数举例

常用原子操作函数举例:

atomic_t v = ATOMIC_INIT(0);     //定义原子变量v并初始化为0
atomic_read(atomic_t *v);        //返回原子变量的值
void atomic_inc(atomic_t *v);    //原子变量增加1
void atomic_dec(atomic_t *v);    //原子变量减少1
int atomic_dec_and_test(atomic_t *v); //自减操作后测试其是否为0,为0则返回true,否则返回false。

代码实操

定义变量:

static atomic_t canopen = ATOMIC_INIT(1);     //定义原子变量并初始化为1

修改open函数:

	if (!atomic_dec_and_test(&canopen))
	{
		atomic_inc(&canopen);//自增
		return -EBUSY;
	}

修改close函数:

int sixth_drv_close(struct inode *inode, struct file *file)
{
	atomic_inc(&canopen);//释放
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	return 0;
}

信号量

信号量(semaphore)是用于保护临界区的一种常用方法,只有得到信号量的进程才能执行临界区代码。
当获取不到信号量时,进程进入休眠等待状态。

使用举例

常用操作函数举例:

//定义信号量
struct semaphore sem;
//初始化信号量
void sema_init (struct semaphore *sem, int val);
void init_MUTEX(struct semaphore *sem);//初始化为0

static DECLARE_MUTEX(button_lock);     //定义互斥锁

//获得信号量
void down(struct semaphore * sem);//没获取到会休眠
int down_interruptible(struct semaphore * sem); 
int down_trylock(struct semaphore * sem);//立刻返回不会睡眠
//释放信号量
void up(struct semaphore * sem);

定义信号量:

static DECLARE_MUTEX(button_lock);     //定义互斥锁

修改open:

static int sixth_drv_open(struct inode *inode, struct file *file)
{

	/* 获取信号量 */
	down(&button_lock);//如果没有获取到则无限等待,休眠

	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}

修改close函数:

int sixth_drv_close(struct inode *inode, struct file *file)
{
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);
	up(&button_lock);
	return 0;
}

阻塞

阻塞操作:是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。
被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。

非阻塞操作 :进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。

我们在open的时候传入O_NONBLOCK则为非阻塞fd = open("...", O_RDWR | O_NONBLOCK);

我们还需要修改open函数:

static int sixth_drv_open(struct inode *inode, struct file *file)
{	
	if (file->f_flags & O_NONBLOCK)//非阻塞
	{
		if (down_trylock(&button_lock))
			return -EBUSY;
	}
	else//阻塞
	{
		/* 获取信号量 */
		down(&button_lock);
	}

	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}

修改read函数:

ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	if (size != 1)
		return -EINVAL;

	if (file->f_flags & O_NONBLOCK)
	{
		if (!ev_press)
			return -EAGAIN;
	}
	else
	{
		/* 如果没有按键动作, 休眠 */
		wait_event_interruptible(button_waitq, ev_press);
	}

	/* 如果有按键动作, 返回键值 */
	copy_to_user(buf, &key_val, 1);
	ev_press = 0;
	
	return 1;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值