第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结构填充进去。
0 | 1 | 2 | 3 | … | major |
---|
完善驱动程序
我们既然有入口函数,我们注册时把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驱动步骤:
- 框架(已搭好)
- 看原理图
- 看2440手册
- 写驱动程序(虚拟地址,用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
之前的裸机中断处理流程
- 按键按下
- CPU发生中断,跳到异常向量入口执行
- b 某函数
函数里面的内容:
1.保存被中断的现场
2.执行中断处理函数
3.恢复
linux下的中断处理流程
对于arm架构来说CPU的异常向量地址可以是0x00000000
,也可以是0xFFFF0000
。我们的内核使用过的是0xFFFF0000
。trap_init函数将异常向量复制到0xFFFF0000
处。
流程:
1.trap_init函数构造异常向量
2.调用恢复。
第007节_字符设备驱动程序之中断方式的按键驱动_Linux异常处理结构_2
单片机下的中断处理:
- 分辨是哪一个中断
- 调用处理函数
- 清中断
单片机中的三个处理流程都是用arm_do_IRQ
这个函数实现的。
linux内核下:
按下按键后
- 进入异常模式 b vector_irq + ____
- __irq_usr
- b asm_do_IRQ
- irq_desc[irq]->handle_irq
- 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
。同样的在应用程序中有pol
l的话,在驱动程序中应该也得有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;
}