STM32MP157驱动开发——异步通信


0.前言

  使用阻塞或者非阻塞的方式来读取驱动中按键值都是应用程序主动读取的,对于非阻塞方式来说还需要应用程序通过 poll 函数不断的轮询。而对于这种类型的需求,最好的处理方式是驱动程序能主动向应用程序发出通知,报告自己可以访问,然后应用程序再从驱动程序中读取或写入数据,类似于中断。Linux 提供了异步通知这个机制来完成此功能,所以这一节就使用异步通信机制,让驱动通知应用程序自己可以被访问。

一、异步通信机制

1.介绍

  首先来回顾一下“中断”机制,中断是处理器提供的一种异步机制,配置好中断以后就可以让处理器去处理其他事情,当中断发生后会触发事先设置好的中断服务函数,在中断服务函数中做具体的处理。例如使用按键来控制蜂鸣器开关,采用中断以后处理器就不需要时刻的去查看按键有没有被按下,因为按键按下以后会自动触发中断,在中断处理函数中进行相应的处理。同样的,Linux 应用程序可以通过阻塞或非阻塞这两种方式来访问驱动设备,通过阻塞方式访问时应用程序会处于休眠态,等待驱动设备可以使用。非阻塞方式则会通过 poll 函数来不断的轮询,查看驱动设备文件是否可以使用。这两种方式都需要应用程序主动的去查询设备的使用情况。
  如果能提供一种类似中断的机制,当驱动程序可以访问时主动告诉应用程序,那么就可以进一步优化相应的驱动访问。所以就使用一种“信号”机制:信号类似于硬件上使用的“中断”,只不过信号是软件层次上的。算是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据。整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断,在整个处理过程中应用程序并没有去查询驱动设备是否可以访问,一切都是由驱动设备自己告诉给应用程序。
  阻塞、非阻塞、异步通知,这三种是针对不同的场合提出来的不同的解决方法,没有优劣之分,在实际的工作和学习中,根据自己的实际需求选择合适的处理方法即可。

2.相关变量和API

  异步通知的核心就是信号,在arch/xtensa/include/uapi/asm/signal.h文件中定义了 Linux 所支持的所有信号,这些信号如下所示:

#define SIGHUP 		1 /* 终端挂起或控制进程终止 */
#define SIGINT 		2 /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 	3 /* 终端退出(Ctrl+\组合键) */
#define SIGILL 		4 /* 非法指令 */
#define SIGTRAP 	5 /* debug 使用,有断点指令产生 */
#define SIGABRT 	6 /* 由 abort(3)发出的退出指令 */
#define SIGIOT 		6 /* IOT 指令 */
#define SIGBUS 		7 /* 总线错误 */
#define SIGFPE 		8 /* 浮点运算错误 */
#define SIGKILL 	9 /* 杀死、终止进程 */
#define SIGUSR1 	10 /* 用户自定义信号 1 */
#define SIGSEGV 	11 /* 段违例(无效的内存段) */
#define SIGUSR2 	12 /* 用户自定义信号 2 */
#define SIGPIPE 	13 /* 向非读管道写入数据 */
#define SIGALRM 	14 /* 闹钟 */
#define SIGTERM 	15 /* 软件终止 */
#define SIGSTKFLT 	16 /* 栈异常 */
#define SIGCHLD 	17 /* 子进程结束 */
#define SIGCONT 	18 /* 进程继续 */
#define SIGSTOP 	19 /* 停止进程的执行,只是暂停 */
#define SIGTSTP 	20 /* 停止进程的运行(Ctrl+Z 组合键) */
#define SIGTTIN 	21 /* 后台进程需要从终端读取数据 */
#define SIGTTOU 	22 /* 后台进程需要向终端写数据 */
#define SIGURG 		23 /* 有"紧急"数据 */
#define SIGXCPU 	24 /* 超过 CPU 资源限制 */
#define SIGXFSZ 	25 /* 文件大小超额 */
#define SIGVTALRM 	26 /* 虚拟时钟信号 */
#define SIGPROF 	27 /* 时钟信号描述 */
#define SIGWINCH 	28 /* 窗口大小改变 */
#define SIGIO 		29 /* 可以进行输入/输出操作 */
#define SIGPOLL 	SIGIO
/* #define SIGLOS 29 */
#define SIGPWR 		30 /* 断点重启 */
#define SIGSYS 		31 /* 非法的系统调用 */
#define SIGUNUSED 	31 /* 未使用信号 */

  这些信号中,除了 SIGKILL(9)和 SIGSTOP(19)这两个信号不能被忽略外,其他的信号都可以忽略。这些信号就相当于中断号,不同的中断号代表了不同的中断,不同的中断所做的处理不同,因此,驱动程序可以通过向应用程序发送不同的信号来实现不同的功能。
  使用中断时需要设置中断处理函数,同样的,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数。

①signal函数

原型

sighandler_t signal(int signum, sighandler_t handler)

参数
signum:要设置处理函数的信号
handler:信号的处理函数
返回值
设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR
信号处理函数原型:

typedef void (*sighandler_t)(int)

例:在使用kill -9 PID命令杀死某一进程时,就是向该进程发送SIGKILL这个信号。当按下键盘上的 CTRL+C 组合键以后会向当前正在占用终端的应用程序发出 SIGINT 信号,SIGINT 信号默认的动作是关闭当前应用程序。
下面的例子功能是改变SIGINT信号的默认处理函数,当按下 CTRL+C 组合键以后先在终端上打印出“SIGINT signal!”这行字符串,然后再关闭当前应用程序。
signaltest.c:

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

void sigint_handler(int num)
{
	printf("\r\nSIGINT signal!\r\n");
	exit(0);
}

int main(void)
{
	signal(SIGINT, sigint_handler);
	while(1);
	return 0;
}

在程序中设置 SIGINT 信号的处理函数为 sigint_handler,当按下 CTRL+C 向 signaltest 发送 SIGINT 信号以后 sigint_handler 函数就会执行,此函数先输出一行“SIGINT signal!”字符串,然后调用 exit 函数关闭 signaltest 应用程序。

②驱动中的信号处理

如果要使用信号处理机制,首先需要在驱动程序中定义一个 fasync_struct 结构体指针变量, fasync_struct 结构体内容如下:

fasync_struct 结构体:
struct fasync_struct {
	spinlock_t fa_lock;
	int magic;
	int fa_fd;
	struct fasync_struct *fa_next;
	struct file *fa_file;
	struct rcu_head fa_rcu;
};

一般将 fasync_struct 结构体指针变量定义到设备结构体中,比如在上一章节的 key_dev 结构体中添加一个 fasync_struct 结构体指针变量:

struct key_dev{
	dev_t devid;
	struct cdev cdev;
	struct class *class;
	......
	struct fasync_struct *async_queue; /* fasync_struct 结构体 */
};
fasync函数

如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数:

int (*fasync) (int fd, struct file *filp, int on)

fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针,fasync_helper 函数原型如下:

int fasync_helper(int fd, 
				  struct file * filp, 
				  int on, 
				  struct fasync_struct **fapp)

fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数是要初始化的 fasync_struct 结构体指针变量。当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”改变 fasync 标记时,驱动程序 file_operations 操作集中的 fasync 函数就会执行。

驱动程序中fasync函数参考示例:

struct xxx_dev {
	......
	struct fasync_struct *async_queue; /* 异步相关结构体 */
};

static int xxx_fasync(int fd, struct file *filp, int on)
{
	struct xxx_dev *dev = (xxx_dev)filp->private_data;

	if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)
		return -EIO;
	return 0;
}

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

在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct,fasync_struct 的释放函数同样为 fasync_helper,release 函数参数参考实例如下:

static int xxx_release(struct inode *inode, struct file *filp)
{
	return xxx_fasync(-1, filp, 0); /* 删除异步通知 */
}
static struct file_operations xxx_ops = {
	......
	.release = xxx_release,
};
kill_fasync函数

当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。 kill_fasync函数负责发送指定的信号:

void kill_fasync(struct fasync_struct **fp, int sig, int band)

参数
fp:要操作的 fasync_struct
sig:要发送的信号
band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT
返回值:无

3.应用程序对异步通知的处理

应用程序对异步通知的处理包括以下三步:
①注册信号处理函数:
  应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。如上文所述。
②将本应用程序的进程号告诉给内核
  使用 fcntl(fd, F_SETOWN, getpid())将本应用程序的进程号告诉给内核
③开启异步通知
  使用如下两行程序开启异步通知:

flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */

注:重点就是通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。

二、驱动程序编写

  在上一节非阻塞IO驱动实验 —— noblockio.c基础上进行开发。在其中加入异步通知相关内容即可,当按键按下以后驱动程序向应用程序发送 SIGIO 信号,应用程序获取到 SIGIO 信号以后读取并且打印出按键值。
  由于并未涉及到外部设备的属性修改,所以不需要修改设备树。直接新建一个目录,将上节的实验程序拷贝后作为模板进行修改:
asyncnoti.c:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/semaphore.h>
#include <linux/of_gpio.h>
#include <linux/of_address.h>
#include <linux/of.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/poll.h>
#include <linux/fcntl.h>

#define KEY_CNT         1
#define KEY_NAME        "key"

/*定义按键状态*/
enum key_status {
    KEY_PRESS = 0,  /*按键按下*/
    KEY_RELEASE,    /*按键释放*/
    KEY_KEEP,       /*按键保持*/
};

/*key设备结构体*/
struct key_dev{
    dev_t devid;                /*设备号*/
    struct cdev cdev;           /*cdev*/
    struct class *class;        /*类*/
    struct device *device;      /*设备*/
    struct device_node *nd;     /*设备节点*/
    int key_gpio;               /*key所使用的的gpio号*/
    struct timer_list timer;    /*定时器*/
    int irq_num;                /*中断号*/
    atomic_t status;            /*按键状态*/
    wait_queue_head_t   r_wait; /*等待队列头*/

    struct fasync_struct *async_queue;  /*fasync_struct结构体*/
};

struct key_dev keydev;          /*创建key设备*/

/*按键防抖处理,也就是开启定时器延时15ms,这里作为中断处理函数*/
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
    mod_timer(&keydev.timer, jiffies + msecs_to_jiffies(15));
    return IRQ_HANDLED;
}

/*解析设备树中的按键属性*/
static int key_parse_dt(void){
    int ret;
    const char *str;

    /*设置Key所使用的GPIO*/
    /*获取设备节点*/
    keydev.nd = of_find_node_by_path("/key");
    if(keydev.nd == NULL){
        printk("keydev node not find!\r\n");
        return -EINVAL;
    }

    /*获取status值*/
    ret = of_property_read_string(keydev.nd, "status", &str);
    if(ret < 0){
        printk("read status failed!\r\n");
        return -EINVAL;
    }
    if(strcmp(str, "okay")){
        printk("key device not okay!\r\n");
        return -EINVAL;
    }

    /*获取compatible属性并匹配*/
    ret = of_property_read_string(keydev.nd, "compatible", &str);
    if(ret < 0){
        printk("read compatible failed!\r\n");
        return -EINVAL;
    }
    if(strcmp(str, "amonter,key")){
        printk("keydev: compatible match failed!\r\n");
        return -EINVAL;
    }

    /*获取设备树中的gpio属性,得到KEY0的设备编号*/
    keydev.key_gpio = of_get_named_gpio(keydev.nd, "key-gpio", 0);
    if(keydev.key_gpio < 0){
        printk("can't get key-gpio");
        return -EINVAL;
    }
    printk("key-gpio num = %d\r\n", keydev.key_gpio);

    /*获取GPIO对应的中断号*/
    keydev.irq_num = irq_of_parse_and_map(keydev.nd, 0);
    if(!keydev.irq_num){
        return -EINVAL;
    }
    printk("irq-num = %d\r\n", keydev.irq_num);

    return 0;
}

static int key_gpio_init(void)
{
    int ret;
    unsigned long irq_flags;

    /*向GPIO子系统申请使用GPIO*/
    ret = gpio_request(keydev.key_gpio, "KEY0");
    if(ret){
        printk(KERN_ERR "keydev: failed to request key-gpio!\r\n");
        return ret;
    }

    /*设置PG3为GPIO输入模式*/
    ret = gpio_direction_input(keydev.key_gpio);
    if(ret < 0){
        printk("can't set gpio mode!\r\n");
        return ret;
    }

    /*获取设备树中指定的中断触发类型*/
    irq_flags = irq_get_trigger_type(keydev.irq_num);
    if(IRQF_TRIGGER_NONE == irq_flags){
        irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;;
    }
    /*申请中断*/
    ret = request_irq(keydev.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);
    if(ret){
        gpio_free(keydev.key_gpio);
        return ret;
    }

    return 0;
}

static void key_timer_function(struct timer_list *arg)
{
    static int last_val = 1;
    int current_val;

    /*读取按键值并判断按键当前状态*/
    current_val = gpio_get_value(keydev.key_gpio);
    if(0 == current_val && last_val){               /*按键按下*/
        atomic_set(&keydev.status, KEY_PRESS);
        wake_up_interruptible(&keydev.r_wait);
        if(keydev.async_queue){
            kill_fasync(&keydev.async_queue, SIGIO, POLL_IN);
        }
    } else if(1 == current_val && !last_val) {      /*按键释放*/
        atomic_set(&keydev.status, KEY_RELEASE);
        wake_up_interruptible(&keydev.r_wait);
        if(keydev.async_queue){
            kill_fasync(&keydev.async_queue, SIGIO, POLL_IN);
        }
    } else{         /*保持*/
        atomic_set(&keydev.status, KEY_KEEP);
    }

    last_val = current_val;
}

/*设备open操作函数*/
static int key_open(struct inode *inode, struct file *filp)
{
    return 0;
}

/*设备read操作函数*/
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int ret;

    if(filp->f_flags & O_NONBLOCK){     /*非阻塞式访问*/
        if(KEY_KEEP == atomic_read(&keydev.status)){
            return -EAGAIN;
        } else {                        /*阻塞式访问*/
            /*加入等待队列,当按键状态改变时,才会被唤醒*/
            ret = wait_event_interruptible(keydev.r_wait, KEY_KEEP != atomic_read(&keydev.status));
            if(ret){
                return ret;
            }
        }
    }
    /* 将按键状态信息发送给应用程序 */
    ret = copy_to_user(buf, &keydev.status, sizeof(int));
    /* 状态重置 */
    atomic_set(&keydev.status, KEY_KEEP);

    return ret;
}

/*设备write操作函数*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

static int key_fasync(int fd, struct file *filp, int on)
{
    return fasync_helper(fd, filp, on, &keydev.async_queue);
}

/*设备release操作函数*/
static int key_release(struct inode *inode, struct file *filp)
{ 
    return key_fasync(-1, filp, 0);
}

/*poll函数,用来处理非阻塞访问*/
static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    poll_wait(filp, &keydev.r_wait, wait);
    if(KEY_KEEP != atomic_read(&keydev.status)){
        mask = POLLIN | POLLRDNORM;     //返回POLLIN
    }

    return mask;
}

/*设备操作函数*/
static struct file_operations key_fops = {
    .owner      = THIS_MODULE,
    .open       = key_open,
    .read       = key_read,
    .write      = key_write,
    .release    = key_release,
    .poll       = key_poll,
    .fasync     = key_fasync,
};

/*驱动入口函数*/
static int __init mykey_init(void){
    int ret;
    /*初始化队列头*/
    init_waitqueue_head(&keydev.r_wait);
    /*初始化按键状态*/
    atomic_set(&keydev.status, KEY_KEEP);

    /*设备树解析*/
    ret = key_parse_dt();
    if(ret){        /*出错*/
        return ret;
    }
    /*gpio中断初始化*/
    ret = key_gpio_init();
    if(ret){    /*出错*/
        return ret;
    }

    /*注册字符设备驱动*/
    /*创建设备号,直接使用系统分配的设备号*/
    ret = alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME);     //由系统分配设备号
    if(ret < 0){
        pr_err("%s couldn't alloc_chrdev_region, ret = %d\r\n", KEY_NAME, ret);
        goto free_gpio;
    }

    /*初始化cdev*/
    keydev.cdev.owner = THIS_MODULE;
    cdev_init(&keydev.cdev, &key_fops);
    /*添加一个cdev*/
    cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
    if(ret < 0){
        goto del_unregister;
    }

    /*创建类*/
    keydev.class = class_create(THIS_MODULE, KEY_NAME);
    if(IS_ERR(keydev.class)){
        goto del_cdev;
    }

    /*创建设备*/
    keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL,KEY_NAME);
    if(IS_ERR(keydev.device)){
        goto destroy_class;
    }

    /*初始化timer,设置定时器处理函数*/
    timer_setup(&keydev.timer, key_timer_function, 0);  //未设置周期,所以不会激活定时期
    return 0;

destroy_class:
    device_destroy(keydev.class, keydev.devid);
del_cdev:
    cdev_del(&keydev.cdev);
del_unregister:
    unregister_chrdev_region(keydev.devid, KEY_CNT);
free_gpio:
    free_irq(keydev.irq_num, NULL);
    gpio_free(keydev.key_gpio);
    return -EIO;
}

/*驱动出口函数*/
static void __exit mykey_exit(void){
    cdev_del(&keydev.cdev);
    unregister_chrdev_region(keydev.devid, KEY_CNT);
    del_timer_sync(&keydev.timer);      /*删除timer*/
    device_destroy(keydev.class, keydev.devid);
    class_destroy(keydev.class);
    free_irq(keydev.irq_num, NULL);     /*释放中断*/
    gpio_free(keydev.key_gpio);         /*释放IO*/
}

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("amonter");
MODULE_INFO(intree, "Y");

修改:
①添加 fcntl.h 头文件,异步通信需要用到相关的API
②在设备结构体中添加 fasync_struct 结构体指针变量
③在定时器处理函数中,当按键状态发生改变时,调用 kill_fasync 函数向应用程序发送 SIGIO 信号,通知应用程序按键数据可以读取了
④在设备操作函数集 file_operations 结构体中添加 key_fasync,在应用程序使用 fasync 时进行调用,在该操作函数中,直接使用 fasync_helper 进行相关处理
⑤在 key_release 函数中,也调用 key_fasync 函数释放 fasync_struct 指针变量

三、编写测试App

  在测试App中,设置 SIGIO 信号的处理函数为 signo_signal_func,当驱动程序向应用程序发送 SIGIO 信号以后,signo_signal_func 函数就会执行。在 signo_signal_func 函数中,通过 read 读取按键值。
asyncnoti_App:

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

static int fd;

/*SIGIO信号处理函数*/
static void sigio_signal_func(int signum)
{
    unsigned int key_val = 0;

    read(fd, &key_val, sizeof(unsigned int));
    if(0 == key_val){
        printf("Key Press!\r\n");
    } else if(1 == key_val){
        printf("Key Release!\r\n");
    }
}

int main(int argc, char *argv[])
{
    int flags = 0;

    /* 判断传参个数是否正确 */
    if(2 != argc) {
        printf("Usage:\n" "\t./asyncKeyApp /dev/key\n");
        return -1;
    }

    /* 打开设备 */
    fd = open(argv[1], O_RDONLY | O_NONBLOCK);
    if(0 > fd) {
        printf("ERROR: %s file open failed!\n", argv[1]);
        return -1;
    }

    /* 设置信号 SIGIO 的处理函数 */
    signal(SIGIO, sigio_signal_func);
    fcntl(fd, F_SETOWN, getpid());      /* 将当前进程的进程号告诉给内核 */
    flags = fcntl(fd, F_GETFD);         /*获取当前的进程状态 */
    fcntl(fd, F_SETFL, flags | FASYNC); /* 设置进程启用异步通知功能 */

    /* 循环轮询读取按键数据 */
    while(1) {
        sleep(2);
    }

    /* 关闭设备 */
    close(fd);
    return 0;
}

①sigio_signal_func 函数,是SIGIO 信号的处理函数,当驱动程序有效按键按下以后就会发送 SIGIO 信号,此函数就会执行。此函数通过 read 函数读取按键状态数据,然后通过 printf 函数打印在终端上
②通过 signal 函数设置 SIGIO 信号的处理函数为 sigio_signal_func
③设置当前进程的状态,开启异步通知的功能
④在 while 循环中等待信号发生

四、运行测试

在这里插入图片描述
可以看到,在实现功能的同时,依然保持较低的CPU占用率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值