中断下的四种使用形式

该文描述了Linux驱动程序中关于按键中断处理LED灯操作的实现,包括按键中断函数、中断处理、阻塞与非阻塞读取、poll机制和异步通知的使用。通过设备树配置GPIO,实现了按键按下时LED灯亮起的功能,并展示了用户空间如何通过驱动读取按键状态。
摘要由CSDN通过智能技术生成

1、按键中断实现led灯操作

        代码的文件框架:


(1)button_drv.c        

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include "button_drv.h"

//存储从内核获得的数据的结构体
struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major;
struct class *key_class;

static struct gpio_desc * led_get_gpiod;



static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//这里先不做处理。
	return 0;
}

static ssize_t gpio_key_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

//获得led的gpiod参数的函数,在led_drv驱动中进行调用
int get_led_gpiod(struct gpio_desc * gpiod)
{
	led_get_gpiod = gpiod;
	return 0;
}

EXPORT_SYMBOL(get_led_gpiod);

//按键中断函数,获得led的gpiod之后,通过gpiod操作函数,对led的gpio引脚进行操作
static irqreturn_t gpio_key_isr (int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
	int val;
	gpiod_direction_output(led_get_gpiod, 0);
	val = gpiod_get_value(gpio_key->gpiod);

	//在这里做按键中断控制led灯的处理,需要在led驱动中获得gpiod的结构体
	if(val)
	{
		gpiod_set_value(led_get_gpiod, val);
	}else
	{
		gpiod_set_value(led_get_gpiod, val);
	}

	printk("Key gpio:%d   val:%d",gpio_key->gpio,val);
    return IRQ_HANDLED;
}



static struct file_operations button_file_operation =
{
	.owner   = THIS_MODULE,
	.open    = gpio_key_drv_open,
	.release = gpio_key_drv_close,
	.read    = gpio_key_drv_read,
	.write   = gpio_key_drv_write,
};

static int gpio_key_probe(struct platform_device *pdev)
{
	int button_count,i,err;
	struct device_node *node = pdev->dev.of_node;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	button_count = of_gpio_count(node);

	if (!button_count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}
	
	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key)*button_count, GFP_KERNEL);
	for(i=0; i<button_count; i++)
	{
		//获得gpio
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node,i,&flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
        	printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
		    return -1;
		}
		//获得gpiod
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;
		
		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
		
		//获得软件中断号,后面根据这个软件中断号注册中断
		gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}
	printk("button_count:%d",button_count);
	for(i=0; i<button_count; i++)
	{
		//函数的入口参数内容:中断号、中断函数、中断的出发方式(也可以传入数值)、传入中断函数的数据。
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "hong_key", &gpio_keys_100ask[i]);
		printk("irq_request:%d",gpio_keys_100ask[i].irq);
	}

	//下面就是设备的创建以及驱动的配置代码了,
    //只是对中断的处理配置是不需要注册设备,注册设备是为了方便对驱动进行操作。
	/*  major = register_chrdev(0, "hong_key", &button_file_operation);
	    key_class = class_create(THIS_MODULE, "hong_key");
	    if (IS_ERR(key_class)) {
		    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		    unregister_chrdev(major, "hong_key");
		    return PTR_ERR(key_class);
	    }
	
	//两个中断函数,只注册了一个驱动,两个按键驱动调用同一个驱动文件。
	//这种方式无法通过查询的方式对按键的数值进行读取。
	    device_create(key_class, NULL, MKDEV(major, 0), NULL, "hong_key_dev%d", i);
	*/
	return 0;
}

static int gpio_key_remove(struct platform_device *pdev)
{
	int button_count,i;
	struct device_node *node = pdev->dev.of_node;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//卸载和创建的函数流程要反着来(最好遵循这个原则,防止出现某些问题)
	//卸载注册的结构体。
	//device_destroy(key_class, MKDEV(major, 0));
	//class_destroy(key_class);
	//unregister_chrdev(major, "hong_key");

	//只需要获得gpio的数量,之后删除对应的中断函数,然后清空申请的结构体内存
	button_count = of_gpio_count(node);
	for(i=0; i<button_count; i++)
	{
		free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
	}
	kfree(gpio_keys_100ask);
	return 0;
}



static const struct of_device_id button_of_device_id[]=
{
	{ .compatible = "hong, gpio_key" },
    { },
};


static struct platform_driver chip_gpio_driver=
{	
	.probe      = gpio_key_probe,
    .remove     = gpio_key_remove,
    .driver     = {
        .name   = "hong_button_dev",
        .of_match_table = button_of_device_id,
    },
};


static int __init gpio_key_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = platform_driver_register(&chip_gpio_driver);
	return err;
}

static void __exit gpio_key_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	platform_driver_unregister(&chip_gpio_driver);
}


module_init(gpio_key_init);
module_exit(gpio_key_exit);

MODULE_LICENSE("GPL");

(2)button_drv.h

#ifndef __button_drv_h__
#define __button_drv_h__


int get_led_gpiod(struct gpio_desc * gpiod);



#endif

(3)led_drv.c

       

#include <linux/module.h>
#include <linux/platform_device.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include "button_drv.h"


static int major;
static struct class * led_class;
static struct gpio_desc * led_gpiod;



static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	char status;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	err = copy_from_user(&status, buf, 1);
	gpiod_set_value(led_gpiod, status);
	return 0;
}



static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}


static int led_drv_open (struct inode *node, struct file *file)
{	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	gpiod_direction_output(led_gpiod, 0);
	return 0;
}



static int led_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}




static struct file_operations led_file_operations =
{
	.owner   = THIS_MODULE,
	.open    = led_drv_open,
	.release = led_drv_close,
	.read    = led_drv_read,
	.write   = led_drv_write,
};

/* 4. 从platform_device获得GPIO
 *    把file_operations结构体告诉内核:注册驱动程序
 */
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//获得系统分析设备树之后,后面使用函数获得GPIO,后面对其进行操作。
	//对照设备树中的“[name]-gpios”中的name,这里为“led-gpios”
	led_gpiod = gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);

	//调用button里面的获得函数,将资源led_gpiod给到按键驱动程序。
	get_led_gpiod(led_gpiod);
	
	major = register_chrdev(0, "hong_led_dev", &led_file_operations);
	led_class = class_create(THIS_MODULE, "hong_led_dev");
	if (IS_ERR(led_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "led");
		gpiod_put(led_gpiod);
		return PTR_ERR(led_class);
	}
	device_create(led_class, NULL, MKDEV(major,0), NULL, "hong_led_treedrv" );
	return 0;
}


static int chip_demo_gpio_remove(struct platform_device *pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//首先要知道设备树中gpios和pinctrl-0指定多个设备
	device_destroy(led_class, MKDEV(major,0)) ;
	class_destroy(led_class);
	unregister_chrdev(major, "hong_led_dev"); 
	return 0;
}




//可以支持多种设备节点的匹配,但是自己还没有试过
static const struct of_device_id led_of_device_id[] =
{
	{ .compatible = "hong, leddrv" },
    { },
};


/* 1. 定义platform_driver */
static struct platform_driver chip_demo_gpio_driver = {
    .probe      = chip_demo_gpio_probe,
    .remove     = chip_demo_gpio_remove,
    .driver     = {
        .name   = "led_drv_test",
        .of_match_table = led_of_device_id,
    },
};



static int __init led_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = platform_driver_register(&chip_demo_gpio_driver);
	return 0;
}



static void __exit led_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	platform_driver_unregister(&chip_demo_gpio_driver);
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");



2、休眠与唤醒和poll

        休眠-唤醒:进去房间陪小孩一起睡觉,小孩醒了会吵醒她。不累,但是妈妈干不了活了。

        这种方式的代码思想是:在read()函数中,采用等待队列事件,在没有被唤醒的时候,就进入休眠的状态(后面的阻塞和非阻塞也要使用这种方法。)。然后会在中断里调用队列事件唤醒函数,对read()里面的休眠函数进行唤醒。对应的驱动代码和用户测试代码如下所示。

        poll:是在用户函数哪里,首先调用poll()函数,将用户线程放进驱动程序中注册的队列中去 ,然后判断是否有事件(这个事件可以由用户自己指定,例如数据缓冲区是否为空)发生,如果没有就开始休眠,(1)如果有,中断唤醒,就休眠结束,回来继续判断是否有事件发生,这时候就会跳出poll循环了,用户程序那边就可以判断到poll函数的返回数据有事件发生,就可以进行事件操作了(例如读取数据);(2)如果没有事件发生,就等待超时以后,内核唤醒,休眠也就结束了,poll函数返回没有事件的操作,用户程序那判断没有事件发生,就处理其他的事情。

(1)button_drv.c

#include <linux/module.h>
#include <linux/poll.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include "button_drv.h"


struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major;
struct class *key_class;

static struct gpio_desc * led_get_gpiod;

//(1)注册一个队列,用于数据阻塞。
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);

// 下面回环存储代码的实现方式比较实用
//回环数据存储的代码,数据必须一个一个的存放,不能一次存放多个
#define BUF_LEN  128
#define NEXT_POS(x)  ( (x + 1) % BUF_LEN )
static int key_data_buf[BUF_LEN];
static int r=0,w=0;


//返回1,说明回环存储区是空的。
static int is_key_buff_empty(void)
{
	return (r == w);
}

//返回1,说明回环存储区满了。
static int is_key_buff_full(void)
{
	return (r == NEXT_POS(w));
}


static void put_key_data(int key)
{
	if(!is_key_buff_full())
	{
		key_data_buf[w]=key;
		w = NEXT_POS(w);
	}
}


static int get_key_data(void)
{
	int key_data;
	if(!is_key_buff_empty())
	{
		key_data = key_data_buf[r];
		r = NEXT_POS(r);
		return key_data;
	}
	return -1;
}



static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int key_data,err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//(2)函数的第二个入口参数是队列唤醒的条件。会将函数本身放进等待队列中,等待唤醒
	//或者!is_key_buff_empty()条件满足,也就是循环数组中有数据。
	wait_event_interruptible(gpio_key_wait, !is_key_buff_empty());
	
	key_data = get_key_data();
	if(key_data == -1)
	{
		printk("the buff is full\n");
	}
	
	err = copy_to_user(buf, &key_data, 4);
	return 0;
}

static ssize_t gpio_key_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

int get_led_gpiod(struct gpio_desc * gpiod)
{
	led_get_gpiod = gpiod;
	return 0;
}

EXPORT_SYMBOL(get_led_gpiod);

static irqreturn_t gpio_key_isr (int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
	int val,gpio_val;
	gpiod_direction_output(led_get_gpiod, 0);
	val = gpiod_get_value(gpio_key->gpiod);

	//在这里做按键中断控制led灯的处理,需要在led驱动中获得gpiod的结构体
	if(val)
	{
		gpiod_set_value(led_get_gpiod, val);
	}else
	{
		gpiod_set_value(led_get_gpiod, val);
	}

	//按键的状态值,只需要8位就可以了。
	gpio_val = ((gpio_key->gpio<<8)|val);
	put_key_data(gpio_val);
	//(3)唤醒队列中放入的用户线程。
	wake_up_interruptible(&gpio_key_wait);

	printk("Key gpio:%d   val:%d",gpio_key->gpio,val);
    return IRQ_HANDLED;
}


static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
	//用户测试函数中,不需要直接调用read()函数进入阻塞状态,而是通过poll()
	//判断是否调用read()函数。
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//将fp线程放入等待队列gpio_key_wait中
	//两种唤醒方式:(1)时间超时,内核将其唤醒。
	//				(2)中断产生中断函数将其唤醒。
	poll_wait(fp, &gpio_key_wait, wait);
	//以存储数据的唤醒数据存储区为是否有时间的判断。
	return is_key_buff_empty()?  0 : POLLIN | POLLRDNORM;
}



static struct file_operations button_file_operation =
{
	.owner   = THIS_MODULE,
	.open    = gpio_key_drv_open,
	.release = gpio_key_drv_close,
	.read    = gpio_key_drv_read,
	.write   = gpio_key_drv_write,
	.poll    = gpio_key_drv_poll,
};

static int gpio_key_probe(struct platform_device *pdev)
{
	int button_count,i,err;
	struct device_node *node = pdev->dev.of_node;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	button_count = of_gpio_count(node);

	if (!button_count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}
	
	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key)*button_count, GFP_KERNEL);
	for(i=0; i<button_count; i++)
	{
		//获得gpio
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node,i,&flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
        	printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
		    return -1;
		}
		//获得gpiod
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;
		
		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
		
		//获得软件中断号,后面根据这个软件中断号注册中断
		gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}
	printk("button_count:%d",button_count);
	for(i=0; i<button_count; i++)
	{
		//函数的入口参数内容:中断号、中断函数、中断的出发方式(也可以传入数值)、传入中断函数的数据。
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "hong_key", &gpio_keys_100ask[i]);
		printk("irq_request:%d",gpio_keys_100ask[i].irq);
	}



	//下面就是设备的创建以及驱动的配置代码,创建设备是为了提供一个按键操作的平台。
	major = register_chrdev(0, "hong_key", &button_file_operation);
	key_class = class_create(THIS_MODULE, "hong_key");
	if (IS_ERR(key_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hong_key");
		return PTR_ERR(key_class);
	}
	
	//两个中断函数,只注册了一个驱动,两个按键驱动调用同一个驱动文件。
	device_create(key_class, NULL, MKDEV(major, 0), NULL, "hong_key_dev0");
	
	return 0;
}

static int gpio_key_remove(struct platform_device *pdev)
{
	int button_count,i;
	struct device_node *node = pdev->dev.of_node;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//卸载和创建的函数流程要反着来(最好遵循这个原则,防止出现某些问题)
	//卸载注册的结构体。
	device_destroy(key_class, MKDEV(major, 0));
	class_destroy(key_class);
	unregister_chrdev(major, "hong_key");

	//只需要获得gpio的数量,之后删除对应的中断函数,然后清空申请的结构体内存
	button_count = of_gpio_count(node);
	for(i=0; i<button_count; i++)
	{
		free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
	}
	kfree(gpio_keys_100ask);
	return 0;
}



static const struct of_device_id button_of_device_id[]=
{
	{ .compatible = "hong, gpio_key" },
    { },
};


//首先利用button_of_device_id结构体数组与设备树进行关联
//之后在调用gpio_key_probe进行设备的创建。
static struct platform_driver chip_gpio_driver=
{	
	.probe      = gpio_key_probe,
    .remove     = gpio_key_remove,
    .driver     = {
        .name   = "hong_button_dev",
        .of_match_table = button_of_device_id,
    },
};


static int __init gpio_key_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = platform_driver_register(&chip_gpio_driver);
	return err;
}

static void __exit gpio_key_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	platform_driver_unregister(&chip_gpio_driver);
}


module_init(gpio_key_init);
module_exit(gpio_key_exit);

MODULE_LICENSE("GPL");





(2)led_test.c

//用于按键中断的设备数据读取测试
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <poll.h>


/*
	./button_test /dev/hong_key_dev0
*/

int main(int argc, char** argv)
{
	//要检测的文件的结构体数组。poll可以检测多个文件
	struct pollfd fds[1];
	int fd,val,poll_return;
	if(argc<2)
	{
		printf("Usage:%s is err\n",argv[0]);
		printf("      ./button_test /dev/hong_key_dev0\n");
		return -1;
	}
	
	fd = open(argv[1],O_RDWR);
	if(fd == -1)
	{
		printf("can not open %s\n",argv[1]);
		return -1;
	}

	/*
	//此程序在读运行的时候,只能在中断运行的时候进行读取,
	//中断不运行的时候,是不能对数据进行读取的。
	//要不然就是在驱动中对中断的次数进行记录,然后将次数传给用户端。
	while(1)
	{
		//这里的循环就不需要延时,read驱动程序中有阻塞睡眠等待
		read(fd,&val,4);
		printf("key_gpio:%d, val:%d\n",(val>>8),(val&0xff));		
	}
	*/
	fds[0].fd = fd;
	fds[0].events = POLLIN;
	while(1)
	{
        //通过对poll函数的判断,当poll驱动函数返回时是对应形式的时候,就可以读取数据了
		poll_return = poll(fds,1,5000);
		if((poll_return == 1)&&(fds[0].revents == POLLIN))
		{
			read(fd,&val,4);
			printf("gpio:%d   val:%d\n",(val>>8),(val&0xff));
		}else
		{
			printf("timer is over\n");
		}
	}
	
	close(fd);
	return 0;
}

3、异步通知

        异步通知: 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈 。妈妈、小孩互不耽误。
异步通知的处理步骤稍微多了一点,如下图任务函数处理框图所示;

 具体驱动代码如下所示:

(1)button_drv.c

        

#include <linux/module.h>
#include <linux/poll.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include "button_drv.h"


struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major;
struct class *key_class;

static struct gpio_desc * led_get_gpiod;

struct fasync_struct *button_fasync;

//注册一个队列,用于数据阻塞。
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);


//回环数据存储的代码,数据必须一个一个的存放,不能一次存放多个
#define BUF_LEN  128
#define NEXT_POS(x)  ( (x + 1) % BUF_LEN )
static int key_data_buf[BUF_LEN];
static int r=0,w=0;


//返回1,说明回环存储区是空的。
static int is_key_buff_empty(void)
{
	return (r == w);
}

//返回1,说明回环存储区满了。
static int is_key_buff_full(void)
{
	return (r == NEXT_POS(w));
}


static void put_key_data(int key)
{
	if(!is_key_buff_full())
	{
		key_data_buf[w]=key;
		w = NEXT_POS(w);
	}
}


static int get_key_data(void)
{
	int key_data;
	if(!is_key_buff_empty())
	{
		key_data = key_data_buf[r];
		r = NEXT_POS(r);
		return key_data;
	}
	return -1;
}



static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int key_data,err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//函数的第二个入口参数是队列唤醒的条件。会将函数本身放进等待队列中,等待唤醒
	//或者!is_key_buff_empty()条件满足,也就是循环数组中有数据。
	//wait_event_interruptible(gpio_key_wait, !is_key_buff_empty());
	
	key_data = get_key_data();
	if(key_data == -1)
	{
		printk("the buff is full\n");
	}
	
	err = copy_to_user(buf, &key_data, 4);
	return 0;
}

static ssize_t gpio_key_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

int get_led_gpiod(struct gpio_desc * gpiod)
{
	led_get_gpiod = gpiod;
	return 0;
}

EXPORT_SYMBOL(get_led_gpiod);

static irqreturn_t gpio_key_isr (int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
	int val,gpio_val;
	gpiod_direction_output(led_get_gpiod, 0);
	val = gpiod_get_value(gpio_key->gpiod);

	//在这里做按键中断控制led灯的处理,需要在led驱动中获得gpiod的结构体
	if(val)
	{
		gpiod_set_value(led_get_gpiod, val);
	}else
	{
		gpiod_set_value(led_get_gpiod, val);
	}

	//按键的状态值,只需要8位就可以了。
	gpio_val = ((gpio_key->gpio<<8)|val);
	put_key_data(gpio_val);
	// 唤醒队列中放入的用户线程。
	//wake_up_interruptible(&gpio_key_wait);
	
	//(4)根据结构体button_fasync->fa_file的变量去调用对应线程的函数。
	kill_fasync(&button_fasync, SIGIO, POLL_IN);
	
	printk("Key gpio:%d   val:%d",gpio_key->gpio,val);
    return IRQ_HANDLED;
}



// (2)晚上fasync对应的函数gpio_key_drv_fasync
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
	//(3)调用fasync_helper()函数设置结构体button_fasync->fa_file的变量。
	if (fasync_helper(fd, file, on, &button_fasync) >= 0)
		return 0;
	else
		return -EIO;
}




static struct file_operations button_file_operation =
{
	.owner   = THIS_MODULE,
	.open    = gpio_key_drv_open,
	.release = gpio_key_drv_close,
	.read    = gpio_key_drv_read,
	.write   = gpio_key_drv_write,
	//(1)在结构体中添加fasync成员。
	.fasync  = gpio_key_drv_fasync,
};

static int gpio_key_probe(struct platform_device *pdev)
{
	int button_count,i,err;
	struct device_node *node = pdev->dev.of_node;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	button_count = of_gpio_count(node);

	if (!button_count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}
	
	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key)*button_count, GFP_KERNEL);
	for(i=0; i<button_count; i++)
	{
		//获得gpio
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node,i,&flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
        	printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
		    return -1;
		}
		//获得gpiod
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;
		
		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
		
		//获得软件中断号,后面根据这个软件中断号注册中断
		gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}
	printk("button_count:%d",button_count);
	for(i=0; i<button_count; i++)
	{
		//函数的入口参数内容:中断号、中断函数、中断的出发方式(也可以传入数值)、传入中断函数的数据。
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "hong_key", &gpio_keys_100ask[i]);
		printk("irq_request:%d",gpio_keys_100ask[i].irq);
	}



	//下面就是设备的创建以及驱动的配置代码,创建设备是为了提供一个按键操作的平台。
	major = register_chrdev(0, "hong_key", &button_file_operation);
	key_class = class_create(THIS_MODULE, "hong_key");
	if (IS_ERR(key_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hong_key");
		return PTR_ERR(key_class);
	}
	
	//两个中断函数,只注册了一个驱动,两个按键驱动调用同一个驱动文件。
	device_create(key_class, NULL, MKDEV(major, 0), NULL, "hong_key_dev0");
	
	return 0;
}

static int gpio_key_remove(struct platform_device *pdev)
{
	int button_count,i;
	struct device_node *node = pdev->dev.of_node;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//卸载和创建的函数流程要反着来(最好遵循这个原则,防止出现某些问题)
	//卸载注册的结构体。
	device_destroy(key_class, MKDEV(major, 0));
	class_destroy(key_class);
	unregister_chrdev(major, "hong_key");

	//只需要获得gpio的数量,之后删除对应的中断函数,然后清空申请的结构体内存
	button_count = of_gpio_count(node);
	for(i=0; i<button_count; i++)
	{
		free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
	}
	kfree(gpio_keys_100ask);
	return 0;
}



static const struct of_device_id button_of_device_id[]=
{
	{ .compatible = "hong, gpio_key" },
    { },
};


//首先利用button_of_device_id结构体数组与设备树进行关联
//之后在调用gpio_key_probe进行设备的创建。
static struct platform_driver chip_gpio_driver=
{	
	.probe      = gpio_key_probe,
    .remove     = gpio_key_remove,
    .driver     = {
        .name   = "hong_button_dev",
        .of_match_table = button_of_device_id,
    },
};


static int __init gpio_key_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = platform_driver_register(&chip_gpio_driver);
	return err;
}

static void __exit gpio_key_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	platform_driver_unregister(&chip_gpio_driver);
}


module_init(gpio_key_init);
module_exit(gpio_key_exit);

MODULE_LICENSE("GPL");





(2)button_test.c

        

//用于按键中断的设备数据读取测试
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>


static int fd;
//(6)编写异步通知的函数。
static void sig_func(int sig)
{
	int val;
	read(fd, &val, 4);
	printf("get button : 0x%x\n", val);
}


/*
	./button_test /dev/hong_key_dev0
*/
//异步通知的方式的代码修改
int main(int argc, char** argv)
{
	//要检测的文件的结构体数组。poll可以检测多个文件
	struct pollfd fds[1];
	int val,poll_return;
	int flags;
	if(argc<2)
	{
		printf("Usage:%s is err\n",argv[0]);
		printf("      ./button_test /dev/hong_key_dev0\n");
		return -1;
	}
	
	//(1)注册对应信号的函数。
	signal(SIGIO,sig_func);	
	//(2)打开设备,和(1)的顺序是都可以的。
	fd = open(argv[1],O_RDWR);
	if(fd == -1)
	{
		printf("can not open %s\n",argv[1]);
		return -1;
	}
	
	//(3)将进程id给驱动
	fcntl(fd, F_SETOWN, getpid());	
	//(4)获得open打开的文件的标志位
	flags = fcntl(fd, F_GETFL);
	//(5)设置获得的标志位中的FASYNC,使其支持异步通知功能
	fcntl(fd, F_SETFL, flags | FASYNC);
	
	while(1)
	{
		printf("test28 \n");
		sleep(3);
	}
	
	close(fd);
	return 0;
}



4、阻塞与非阻塞

        阻塞与非阻塞的实现,也是要靠前面介绍的队列中的中断事件等待函数实现的。在open一个设备节点的时候,在内核就会创建struct file结构体,结构体中的f_flags参数代表打开的这个驱动函数进行调用的时候,是否允许阻塞,这个可以在open()驱动设备的时候进行设置,也通过fcntl()函数获得此标志,然后对此标志进行配置。

(1)button_drv.c

        

#include <linux/module.h>
#include <linux/poll.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include "button_drv.h"


struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
static int major;
struct class *key_class;

static struct gpio_desc * led_get_gpiod;

struct fasync_struct *button_fasync;

//注册一个队列,用于数据阻塞。
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);


//回环数据存储的代码,数据必须一个一个的存放,不能一次存放多个
#define BUF_LEN  128
#define NEXT_POS(x)  ( (x + 1) % BUF_LEN )
static int key_data_buf[BUF_LEN];
static int r=0,w=0;


//返回1,说明回环存储区是空的。
static int is_key_buff_empty(void)
{
	return (r == w);
}

//返回1,说明回环存储区满了。
static int is_key_buff_full(void)
{
	return (r == NEXT_POS(w));
}


static void put_key_data(int key)
{
	if(!is_key_buff_full())
	{
		key_data_buf[w]=key;
		w = NEXT_POS(w);
	}
}


static int get_key_data(void)
{
	int key_data;
	if(!is_key_buff_empty())
	{
		key_data = key_data_buf[r];
		r = NEXT_POS(r);
		return key_data;
	}
	return -1;
}



static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int key_data,err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    //在没有阻塞的情况下,在缓冲区没有数据的时候,直接将函数返回,结束函数。
    //当状态设置为有阻塞的情况下,这部分不会运行,就会运行下面的等待中断事件的函数。
	if(is_key_buff_empty()&&(file->f_flags & O_NONBLOCK))
	{
		return -EAGAIN;
	}
	
	//函数的第二个入口参数是队列唤醒的条件。会将函数本身放进等待队列中,等待唤醒
	//或者!is_key_buff_empty()条件满足,也就是循环数组中有数据。
	//阻塞和非阻塞中的阻塞还是需要在自己通过代码实现,就利用下面的中断等待函数实现。
	wait_event_interruptible(gpio_key_wait, !is_key_buff_empty());

	key_data = get_key_data();
	if(key_data == -1)
	{
		printk("the buff is full\n");
	}
	
	err = copy_to_user(buf, &key_data, 4);
	return 0;
}

static ssize_t gpio_key_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

static int gpio_key_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	return 0;
}

int get_led_gpiod(struct gpio_desc * gpiod)
{
	led_get_gpiod = gpiod;
	return 0;
}

EXPORT_SYMBOL(get_led_gpiod);

static irqreturn_t gpio_key_isr (int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
	int val,gpio_val;
	gpiod_direction_output(led_get_gpiod, 0);
	val = gpiod_get_value(gpio_key->gpiod);

	//在这里做按键中断控制led灯的处理,需要在led驱动中获得gpiod的结构体
	if(val)
	{
		gpiod_set_value(led_get_gpiod, val);
	}else
	{
		gpiod_set_value(led_get_gpiod, val);
	}

	//按键的状态值,只需要8位就可以了。
	gpio_val = ((gpio_key->gpio<<8)|val);
	put_key_data(gpio_val);
	
	//唤醒队列中放入的用户线程。
	wake_up_interruptible(&gpio_key_wait);
	
	printk("Key gpio:%d   val:%d",gpio_key->gpio,val);
    return IRQ_HANDLED;
}




static struct file_operations button_file_operation =
{
	.owner   = THIS_MODULE,
	.open    = gpio_key_drv_open,
	.release = gpio_key_drv_close,
	.read    = gpio_key_drv_read,
	.write   = gpio_key_drv_write,
};

static int gpio_key_probe(struct platform_device *pdev)
{
	int button_count,i,err;
	struct device_node *node = pdev->dev.of_node;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	button_count = of_gpio_count(node);

	if (!button_count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}
	
	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key)*button_count, GFP_KERNEL);
	for(i=0; i<button_count; i++)
	{
		//获得gpio
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node,i,&flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
        	printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
		    return -1;
		}
		//获得gpiod
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;
		
		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
		
		//获得软件中断号,后面根据这个软件中断号注册中断
		gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}
	printk("button_count:%d",button_count);
	for(i=0; i<button_count; i++)
	{
		//函数的入口参数内容:中断号、中断函数、中断的出发方式(也可以传入数值)、传入中断函数的数据。
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "hong_key", &gpio_keys_100ask[i]);
		printk("irq_request:%d",gpio_keys_100ask[i].irq);
	}



	//下面就是设备的创建以及驱动的配置代码,创建设备是为了提供一个按键操作的平台。
	major = register_chrdev(0, "hong_key", &button_file_operation);
	key_class = class_create(THIS_MODULE, "hong_key");
	if (IS_ERR(key_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hong_key");
		return PTR_ERR(key_class);
	}
	
	//两个中断函数,只注册了一个驱动,两个按键驱动调用同一个驱动文件。
	device_create(key_class, NULL, MKDEV(major, 0), NULL, "hong_key_dev0");
	
	return 0;
}

static int gpio_key_remove(struct platform_device *pdev)
{
	int button_count,i;
	struct device_node *node = pdev->dev.of_node;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//卸载和创建的函数流程要反着来(最好遵循这个原则,防止出现某些问题)
	//卸载注册的结构体。
	device_destroy(key_class, MKDEV(major, 0));
	class_destroy(key_class);
	unregister_chrdev(major, "hong_key");

	//只需要获得gpio的数量,之后删除对应的中断函数,然后清空申请的结构体内存
	button_count = of_gpio_count(node);
	for(i=0; i<button_count; i++)
	{
		free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
	}
	kfree(gpio_keys_100ask);
	return 0;
}



static const struct of_device_id button_of_device_id[]=
{
	{ .compatible = "hong, gpio_key" },
    { },
};


//首先利用button_of_device_id结构体数组与设备树进行关联
//之后在调用gpio_key_probe进行设备的创建。
static struct platform_driver chip_gpio_driver=
{	
	.probe      = gpio_key_probe,
    .remove     = gpio_key_remove,
    .driver     = {
        .name   = "hong_button_dev",
        .of_match_table = button_of_device_id,
    },
};


static int __init gpio_key_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = platform_driver_register(&chip_gpio_driver);
	return err;
}

static void __exit gpio_key_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	platform_driver_unregister(&chip_gpio_driver);
}


module_init(gpio_key_init);
module_exit(gpio_key_exit);

MODULE_LICENSE("GPL");

(2)button_test.c

//用于按键中断的设备数据读取测试
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>



/*
	./button_test /dev/hong_key_dev0
*/
//异步通知的方式的代码修改
int main(int argc, char** argv)
{
	//要检测的文件的结构体数组。poll可以检测多个文件
	struct pollfd fds[1];
	int val,poll_return;
	int flags,i,fd;
	ssize_t err;
	if(argc<2)
	{
		printf("Usage:%s is err\n",argv[0]);
		printf("      ./button_test /dev/hong_key_dev0\n");
		return -1;
	}
	
	fd = open(argv[1],O_RDWR|O_NONBLOCK);
	if(fd == -1)
	{
		printf("can not open %s\n",argv[1]);
		return -1;
	}
	
	for(i=0; i<10; i++)
	{
		//read()函数的默认状态是非阻塞态,
		//所以下面的read()函数,在没有获得数据的时候,会直接返回。
		err = read(fd, &val, 4);
		if(err == 4)
		{
			printf("get data:%d\n",val);
		}else
		{
			printf("not get data\n");
		}
	}
	
	//(4)获得open打开的文件的标志位
	flags = fcntl(fd, F_GETFL);
	//(5)设置获得的标志位中的FASYNC,使其支持异步通知功能
	fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
	
	while(1)
	{
		//因为上面设置了结构体的成员的阻塞的标志位,
		//所以下面的read()函数在没有获得数据的时候,就会进入阻塞态
		if(read(fd, &val, 4) == 4)
		{
			printf("get val:%d\n",val);
		}else
		{
			printf("while not get val\n");
		}
	}
	
	close(fd);
	return 0;
}



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值