一、查询方式的按键驱动程序
查询方式的按键驱动程序,与LED驱动程序类似,我们来复习一下上节的写好的字符设备驱动程序框架,改写出查询方式的按键驱动程序。
(1)按键驱动程序如下:
Open中配置引脚
Read中返回引脚状态
入口函数:地址映射 虚拟地址
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include //相关函数的头文件
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); //注册驱动程序中的方法,生成以主设备号为索引,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); //标记second_drv_init为入口函数,设备注册驱动insmod的时候,调用该方法
module_exit(second_drv_exit); //标记second_drv_exit为出口函数,设备注册驱动rmmod的时候,调用该方法
MODULE_LICENSE("GPL"); //遵循"GPL"协议,不写会产生一些错误
(2)APP应用测试程序:
#include
#include
#include
#include
/* seconddrvtest
*/
int main(int argc, char **argv)
{
int fd;
unsigned char key_vals[4];
int cnt = 0;
fd = open("/dev/buttons", O_RDWR); //打开按键设备节点
if (fd < 0)
{
printf("can't open!
");
}
while (1)
{
read(fd, key_vals, sizeof(key_vals)); //从读取内核空间传来的数据并打印
if (!key_vals[0] || !key_vals[1] || !key_vals[2] || !key_vals[3])
{
printf("%04d key pressed: %d %d %d %d
", cnt++, key_vals[0], key_vals[1], key_vals[2], key_vals[3]);
}
}
return 0;
}
(3)实验结果
查询方式的按键程序,极大的耗费了资源。
(4)需要注意的地方
1.驱动中我们在read中使用了copy_to_user方法,因为read方法的参数列表适合于copy_to_user方法。
使用时参数列表的buf、size是测试程序传进来的,我们不需要定义,是形参与用户空间的参数对应。
我们定义一个key_vals数组,对其赋值,然后将其值传递给用户空间的buf中。
用户调用
read(fd, key_vals, sizeof(key_vals));
驱动程序调用second_drv_read(fd,buf,4),实现内核驱动与应用之间数据传递
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;
copy_to_user(buf, key_vals, sizeof(key_vals));
return sizeof(key_vals);
}
2file_operations结构体中涉及的方法:
3.top命令类似于window下任务管理器可以查看各个进程占用资源的情况
4.查看寄存器的值regval :
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
regval = *gpfdat;
二、中断方式的按键驱动程序
(1)说明:
使用中断方式,那么肯定有一个中断的初始化注册,就是告诉内核我按下按键的时候会触发一个中断,同时一定有一个中断处理函数来处理中断发生时应该做什么。
linux内核中 如何告诉内核我按下按键了给我触发中断并实现中断处理函数呢。
使用的函数如下:
request_irq():open驱动程序时调用
free_irq():卸载驱动程序时解除按键中断
1.注册中断函数原型在Kernel/irq/manage.c中定义:
intrequest_irq(unsigned int irq, irq_handler_t
handler,
unsigned long irqflags, const char *devname,void *dev_id)
void *:void即“无类型”,void
*则为“无类型指针”,可以指向任何数据类型。
参数1:中断号
参数2:向系统注册的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id参数将被传递给它
参考内核中如下函数
参数3:触发方式,在下面定义了一些宏
参数4:设置中断名称,通常是设备驱动程序的名称 在cat /proc/interrupts中可以看到此名称。
参数5:设备ID,卸载时用,在中断共享时也会用到,一般设置为这个设备的设备结构体或者NULL
request_irq()返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
2.注销函数定义在Kernel/irq/manage.c中定义:
void free_irq(unsigned int irq, void *dev_id)
返回值:函数运行正常时返回 0 ,否则返回对应错误的负值。
(2)中断方式的按键驱动代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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);
/* 中断事件标志, 中断服务程序buttons_irq将它置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");
(3)驱动编译的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 += third_drv.o
(4)驱动测试应用程序
#include
#include
#include
#include
/* thirddrvtest
*/
int main(int argc, char **argv)
{
int fd;
unsigned char key_val;
fd = open("/dev/buttons", O_RDWR);
if (fd < 0)
{
printf("can't open!
");
}
while (1)
{
read(fd, &key_val, 1);
printf("key_val = 0x%x
", key_val);
}
return 0;
}
(5)运行结果
(6)需要注意的地方
1.wait_event_interruptible(button_waitq, ev_press);
这个函数会判断ev_press这个参数,如果=0,进入休眠,不返回,程序停止在此处。当被唤醒时,从此处继续执行。
button_waitq:static DECLARE_WAIT_QUEUE_HEAD(button_waitq);我的理解是这个button_waitq代表了这个应用程序的进程,被挂载到DECLARE_WAIT_QUEUE_HEAD某个队列中。
如果没有这个休眠操作,程序read的时候,会不断返回一个老值,并不能降低CPU资源。
当中断发生,会执行中断处理函数,此时唤醒队列中次应用的进程,继续执行,返回结果。
ev_press = 1; /* 表示中断发生了 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */
2.不写测试程序如何打开某个设备节点:
exec 5会调用open
释放应用程序:
exec 5<&- 会调用release
3查看当前进程:ps(可以查看进程状态 s:休眠)
三、加入poll机制
上一节我们实现了中断的按键驱动程序。CPU占用已经很低,且没有按键的时候会休眠,那么我们为什么还需要poll机制呢。之前的测试程序是这样:
while (1)
{
read(fd, &key_val, 1);
printf("key_val = 0x%x
", key_val);
}
在read中ev_press=0;进程被休眠,程序停止在这里,当中断发生后,进程被唤醒,继续执行copy_to_user(buf, &key_val, 1);
ev_press = 0;
进行返回,这个时候测试程序返回,然后重新进入read函数。也就是说没有poll机制,大部分时间程序都处在read中休眠的那个位置。如果我们不想让程序停在这个位置,而是希望当有按键按下时,我们再去read,因此我们编写poll函数,测试程序调用poll函数根据返回值,来决定是否执行read函数。
(1)驱动程序加入:
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;
}
forth_drv_poll是sys_poll中的一部分,在里面被调用。ev_press默认为0,此时驱动中的poll返回默认值0,会发生休眠。
当中断发生,ev_press=1,此时返回值不是0,会调用read函数。
(2)测试代码:
#include
#include
#include
#include
#include
/* 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!
");
}
fds[0].fd = fd;
fds[0].events = POLLIN;
while (1)
{
ret = poll(fds, 1, 5000);
if (ret == 0)
{
printf("time out
");
}
else
{
read(fd, &key_val, 1);
printf("key_val = 0x%x
", key_val);
}
}
return 0;
}
(3)需要注意的地方
1.如果超时时间到了也会返回,然后根据返回值决定是否执行read函数。上一节的read没有按键按下一直不会返回。
2.期待得到POLLIN这个返回值,代表有数据可读。是一个宏,根据自己可能出现的现象来定义。
3.驱动中的forth_drv_poll是sys_poll中的一部分,应用程序调用poll,并非简单的调用驱动中的poll
4.forth_drv_poll的返回值,决定了sys_poll的返回值。中断发生,返回值为POLLIN,即有数据可读,根据返回值执行read函数。