基于Linux的字符设备驱动框架(使用中断、poll、异步通知、同步互斥阻塞)

1 字符设备驱动

所用内核:Linux-2.6.22.6

  1. 执行 insmod xxx.ko 加载驱动程序
  2. 执行 rmmod xxx 卸载驱动程序
  3. 执行 cat /proc/devices 查看内核支持驱动程序和主设备号
  4. 执行 cat /proc/interrupts 查看中断
  5. 执行 mknod /dev/xxx c major 0 创建设备节点
  6. 执行 ls /dev/ 查看设备节点

1.1 驱动框架

当APP调用open、write、read…时,对应的内核虚拟文件系统(VFS)便会调用sys_open、sys_write、sys_read…,然后这些系统函数会去调用驱动中对应的xxx_open、xxx_write、xxx_read…,在驱动程序中需要实现这些函数的功能以便内核调用。


#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>

/* volatile unsigned long *gpfcon = NULL; */
/* volatile unsigned long *gpfdat = NULL; */
struct class *xxx_class;
struct class_device *xxx_class_dev;

/* 应用程序对设备文件 /dev/xxx 执行open(...)时,
 * 就会调用 xxx_open函数
 */
static int xxx_open(struct inode *inode, struct file *file)
{
	/* xxx_open函数实现 */
...
	return 0;
}

/* 应用程序对设备文件 /dev/xxx 执行write(...)时,
 * 就会调用 xxx_write函数
 */
static ssize_t xxx_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(&val, buf, count);	/* 用户空间从内核空间得到数据 */

	/* 对用户空间传回的值进行处理 */
...
	return 0;
}

/* 这个结构是字符设备驱动程序的核心 
 * 当应用程序操作设备文件时所调用的open、read、write等函数,
 * 最终会调用这个结构中指定的对应函数。
 * 结构体 file_operations 中存放的都是函数指针
 */
static struct file_operations xxx_fops = {
	.owner	= THIS_MODULE,		/* 这是一个宏,指向编译模块时自动创建 __this_module变量 */
	.open	= xxx_open,	
	.write	= xxx_write,
    .read	= xxx_read,
    .poll	= xxx_poll,		/* 处理poll机制 */
    .fasync	= xxx_fasync,	/* 处理异步通知 */
};

#define DEVICE_NAME     "xxx"  /* 加载模式后,执行”cat /proc/devices”命令后可以看到的设备名称 */
#define xxx_MAJOR       0      /* 主设备号, 设为 0 是为了让系统自动分配主设备号,其值也可以自己设定 */
static struct class *leds_class;	/* 创建类 */
static struct class_device	*leds_class_devs; /* 在创建的类下创建设备节点 */

int major;	/* 让系统自动分配主设备号 */
/* 执行 insmod 命令时就会调用这个函数 */
int xxx_init(void)
{
	/* 注册字符设备,注册完后只会将 xxx_fops 填充进数组,但是并不会生成系统信息
	 * 参数为主设备号、设备名字、file_operations结构
	 * 这样主设备号就和具体的 file_operations 结构联系起来了
	 * 操作主设备号为 xxx_MAJOR 的设备文件时,就会调用 xxx_fops 中的相关成员函数
     * 函数原型:int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) 
     */
	major = register_chrdev(xxx_MAJOR, DEVICE_NAME, &xxx_fops);	

    /* 以下用来创建系统信息 
     * 函数原型:struct class *class_create(struct module * owner, const char * name);
     */
	xxx_class = class_create(THIS_MODULE, DEVICE_NAME);	/* 创建 DEVICE_NAME 类 */
    if (IS_ERR(xxx_class))	/* 判断创建成功与否 */
		return PTR_ERR(xxx_class);
    
    /* 函数原型:struct class_device *class_device_create(struct class * cls, struct class_device * parent, dev_t devt, struct device * device, const char * fmt,...); */
	xxx_class_dev = class_device_create(xxx_class, NULL, MKDEV(major, 0), NULL, "xxx"); /* majior 为主设备号,0 为次设备号(可以设为其他值), 在 DEVICE_NAME 类 下创建 xxx 的设备节点, 存在于 /dev/xxx */
    if (unlikely(IS_ERR(xxx_class_dev)))	/* 判断创建成功与否 */
		return PTR_ERR(xxx_class_dev);
    
	/* 当要操作开发板中的寄存器时,需要通 ioremap 来进行地址映射
	 * 函数原型:void __iomem *ioremap (unsigned long phys_addr, unsigned long size)
	 * gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	 * gpfdat = gpfcon + 1;
	*/
    
	return 0;
}

/* 执行 rmmod 命令时就会调用这个函数 */
static void xxx_exit(void)
{
	unregister_chrdev(major, DEVICE_NAME);	  /* 卸载驱动程序,函数原型:int unregister_chrdev(unsigned int major, const char *name) */
	class_device_unregister(xxx_class_dev);	  /* 卸载设备节点,函数原型:void class_device_unregister(struct class_device *class_dev) */
	class_destroy(xxx_class);				 /* 卸载创建的类,函数原型:void class_destroy(struct class *cls) */
	/* iounmap(gpfcon);						取消地址映射,函数原型:void iounmap(void *addr) */
}

/* 指定驱动程序的初始化函数和卸载函数 */	
module_init(xxx_init);	
module_exit(xxx_exit);

/* 描述驱动程序的一些信息,不是必须的 */
MODULE_LICENSE("GPL");

1.1.1 应用程序(main)

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

int main(int argc, char **argv)
{
	int fd;
	fd = open("/dev/xxx", O_RDWR);	/* 打开设备节点, xxx需要和驱动中创建的设备节点相同*/
	if (fd < 0)		/* 判断设备节点是否存在 */
	{
		printf("can't open!\n");
	}

    /* 一些其他的处理
     * write(fd, void *buf, int buflen)   向内核写值 
     * read(fd, char __user * userbuf, size_t count)  从内核读取值
     */
	return 0;
}

1.2 驱动中加中断

因为查询方式太耗费CPU资源(99%),所以用中断方式来减少对应用程序对CPU的消耗。

/* 在驱动框架中添加 */
#include <linux/irq.h>

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

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

static DECLARE_WAIT_QUEUE_HEAD(xxx_waitq);	/* 定义等待队列头结构体 */

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

/* 中断处理函数, 当发生中断了就会执行该函数 */
static irqreturn_t xxx_irq(int irq, void *dev_id)
{
	/* 一些中断处理程序 */
 ...
     ev_press = 1; 	/* 表示中断发生了 */
	 wake_up_interruptible(&xxx_waitq); 	/* 当中断发生了就唤醒休眠的进程 */
	
	return IRQ_RETVAL(IRQ_HANDLED);
}

static int xxx_drv_open(struct inode *inode, struct file *file)
{
	/* 注册中断 
	 * int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *devname, void *dev_id)
	 */
	request_irq(IRQ_EINT0,  xxx_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
 ...
	return 0;
}

ssize_t xxx_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	/* 如果没有中断发生,就休眠 */
	wait_event_interruptible(xxx_waitq, ev_press);	/* 判断 ev_press的值,如果 ev_press = 0 就会休眠,如果休眠被唤醒就会从这继续向下执行程序 */

	/* 如果有按键按动作
	 * 一些处理程序 
	 * copy_to_user(buf, &key_val, 1);
	 */
 ...
	ev_press = 0;	/* 将中断标志清 0, 让程序休眠 */
	
	return 1;
}

/* 关闭设备时,用来释放中断 */
int xxx_close(struct inode *inode, struct file *file)
{
    /* 释放中断 
     * void free_irq(unsigned int irq, void *dev_id)
     */
	free_irq(IRQ_EINT0, &pins_desc[0]);
...
	return 0;
}

static struct file_operations xxx_fops = {
...
    /* 为释放中断添加函数 */
	.release = third_drv_close,
};

1.2.1 应用程序(main)

/* 添加以下代码 */
int main(int argc, char **argv)
{
	int fd;
	unsigned char key_val;

	fd = open("/dev/xxx", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}
	while (1)	/* 一直查询有无中断产生, */
	{
		read(fd, &key_val,1);	/* 如果没有中断产生永远都不会返回 */
		printf("key_val = 0x%x\n", key_val);
	}

1.3 驱动中加poll机制

poll机制也是一种查询,但是它在查询过程中也可以休眠,如果有事件发生了就立刻返回,若在规定时间内没有事件发生就会返回(中断是只有中断发生之后才返回),对于系统调用 poll 或 select,它们对应的内核函数都是sys_poll。

/* 在驱动框架中添加 */
#include <linux/poll.h>

static DECLARE_WAIT_QUEUE_HEAD(xxx_waitq);

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

static irqreturn_t xxx_irq(int irq, void *dev_id)
{
	/* 和1.2中的xxx_irq相同 */
}

ssize_t xxx_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	/* 和1.2中的xxx_drv_read相同 */
}

static unsigned xxx_poll(struct file *file, poll_table *wait)
{
	unsigned int mask = 0;
	poll_wait(file, &xxx_waitq, wait);	/* 不会立即休眠,将进程挂到 xxx_waitq 队列中去  函数原型:static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)*/

	if (ev_press)
	{
		mask |= POLLIN | POLLRDNORM;
	}
	return mask;
}

static struct file_operations xxx_fops = {
...
    /* 为 poll 添加函数 */
	.poll 	 = xxx_poll,
};

1.3.1 应用程序(main)

/* 添加以下代码 */
#include <poll.h>

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

	struct pollfd fds[1];	/* poll 机制可以同时查询多个驱动程序 */
    /* struct pollfd {
			int fd;
			short events;
			short revents;
	}; */
	fds[0].fd		= fd;
	fds[0].events	= POLLIN;	/* POLLIN 表示期待有数据读取 */
	while (1)
	{
		ret = poll(fds, 1, 5000); /* 函数原型:static unsigned int poll(struct file *file, struct socket *sock, poll_table *wait) */
		if (ret == 0)	/* ret=0 表示超时 */
		{
			printf("time out!\n");
		}
		else
		{
			read(fd, &key_val,1);
			printf("key_val = 0x%x\n", key_val);
		}
	}
	
	return 0;
}

1.4 驱动中加异步通知

查询、中断和poll 都是应用程序主动地去查询。异步通知为当有事件发生时驱动程序就去会提醒应用程序读取相应的值,它使用signal(信号量,用于进程间发信号)

进程间发信号的要点

  1. 应用程序:注册信号处理函数
  2. 谁发: 驱动发
  3. 发给谁: 发给应用程序(应用程序要告诉驱动PID)
  4. 怎么发: kill_fasync
/* 在驱动框架中添加 */
static struct fasync_struct *xxx_async;
/* struct fasync_struct {
	int	magic;
	int	fa_fd;
	struct	fasync_struct	*fa_next;  //singly linked list 
	struct	file 		*fa_file;
}; */
...
static irqreturn_t xxx_irq(int irq, void *dev_id)
{
...
     ev_press = 1; 	/* 表示中断发生了 */
	 wake_up_interruptible(&xxx_waitq); 	/* 唤醒休眠的进程 */
	
	 /* 3、在设备资源可获得时,调用kill_fasync()函数激发相应的信号 
	 * 函数原型:void kill_fasync(struct fasync_struct **fp, int sig, int band)
	 */
	 kill_fasync(&xxx_async, SIGIO, POLL_IN);
	
	return IRQ_RETVAL(IRQ_HANDLED);
}
...
static int xxx_fasync(int fd, struct file *filp, int on)
{
    /* 函数原型:int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp) */
	return fasync_helper(fd, filp, on, &xxx_async);
}

static struct file_operations xxx_fops = {
...
	.fasync  = xxx_fasync,
};

1.4.1 应用程序(main)

/* 添加以下代码 */
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

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);
	
}

/* fasync */
int main(int argc, char **argv)
{
	unsigned char key_val;
	int ret;
	int oflags;

	signal(SIGIO, my_signal_fun);  /* SIGIO表示IO口有数据,sighandler_t signal(int signum, sighandler_t handler); */

	fd = open("/dev/xxx", O_RDWR);
	if (fd < 0)
	{
		printf("can't opren!\n");
	}

	fcntl(fd, F_SETOWN, getpid());	 /* 1、告诉内核,发给谁, 在 F_SETOWN 控制命令处理中设置filp->f_owner为对应进程ID ,此项工作已由内核完成,设备驱动无须处理*/
	oflags = fcntl(fd, F_GETFL);	/* 修改标志 */
	fcntl(fd, F_SETFL, oflags | FASYNC); /* 2、支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的xxx_fasync()函数将得以执行,驱动中应该实现xxx_fasync()函数 */	
 
	while (1)
	{
		sleep(1000);
	}
	
	return 0;
}

1.5 驱动中加同步互斥阻塞

1.5.1 原子操作

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

  1. atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为 0
  2. atomic_read(atomic_t *v); //返回原子变量的值
  3. void atomic_inc(atomic_t *v); //原子变量增加 1
  4. void atomic_dec(atomic_t *v); //原子变量减少 1
  5. int atomic_dec_and_test(atomic_t *v); //自减操作后测试其是否为0,为0则返回true,否则返回false
/* 在驱动框架中添加 */
...
static atomic_t canopen = ATOMIC_INIT(1);		/* 定义原子变量并初始化为 1 */
...
static int xxx_drv_open(struct inode *inode, struct file *file)
{
	if (!atomic_dec_and_test(&canopen))	/* 只能允许一个应用程序运行,后面要使用应用程序将会被杀死 */
	{
		atomic_inc(&canopen);
		return -EBUSY;
	}
...
    
	return 0;
}
  
int xxx_drv_close(struct inode *inode, struct file *file)
{
	atomic_inc(&canopen);
...
	return 0;
}

1.5.2 信号量

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

  1. 定义信号量

    struct semaphore sem;

  2. 初始化信号量

    void sema_init (struct semaphore *sem, int val);

    void init_MUTEX(struct semaphore *sem); //初始化为0

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

  3. 获得信号量
    void down(struct semaphore * sem);
    int down_interruptible(struct semaphore * sem);
    int down_trylock(struct semaphore * sem);

  4. 释放信号量
    void up(struct semaphore * sem);

/* 在驱动框架中添加 */
...
static DECLARE_MUTEX(xxx_lock);	/* 定义互斥锁 */
...
static int xxx_drv_open(struct inode *inode, struct file *file)
{
		/* 获取信号量 */
		down(&xxx_lock);	/* 第一个应用程序会获得信号量,后面的应用程序将不会获得信号量从而进入休眠(前面的应用程序用完后将顺序获得信号量) */
...
	return 0;
}

int xxx_drv_close(struct inode *inode, struct file *file)
{
...
	up(&xxx_lock);	/* 释放信号量 */
	return 0;
}

1.5.3 阻塞和非阻塞

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

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

fd = open("…", O_RDWR | O_NONBLOCK); 传入 O_NONBLOCK 参数则为非阻塞,不传入 O_NONBLOCK 则为阻塞

/* 在驱动框架中添加 */
...
static int xxx_drv_open(struct inode *inode, struct file *file)
{
	if (file->f_flags & O_NONBLOCK)		/* 非阻塞 */
	{
		if (down_trylock(&xxx_lock))	/* 如果无法获取信号量就立马返回 */
		{
			return -EBUSY;
		}
	}
	else	/* 阻塞 */
	{
		/* 获取信号量 */
		down(&xxx_lock);	/* 如果无法获取信号量就进入休眠状态 */
	}
...
	return 0;
}

ssize_t xxx_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
...
	if (file->f_flags & O_NONBLOCK)		/* 非阻塞 */
	{
		if (!ev_press)
		{
			return -EAGAIN;
		}
	}
	else
	{
		/* 如果没有按键按动作,就休眠 */
		wait_event_interruptible(button_waitq, ev_press);
	}
...
	ev_press = 0;
	return 1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值