i.MX 6ULL 驱动开发 九:中断
一、GICv2 基本概念
GICv2 整理_lqonlylove的博客-CSDN博客
二、Linux 中断子系统和中断上下部
Linux 驱动开发 三十九:Linux 中断(一)_lqonlylove的博客-CSDN博客_linux msi中断
三、设备树关于中断属性说明
Linux 驱动开发 九:《Power_ePAPR_APPROVED_v1.12》翻译_lqonlylove的博客-CSDN博客
Linux 驱动开发 十:《Devicetree Specification》翻译_lqonlylove的博客-CSDN博客
Linux 驱动开发 三十七:《gic.txt》翻译_lqonlylove的博客-CSDN博客
四、imx6ull在设备树中添加中断属性说明
Linux 驱动开发 三十八:《fsl-imx-gpio.txt》翻译_lqonlylove的博客-CSDN博客
五、原理图
通过原理图分析可以得到,当按键按下后 KEY0
为低电平,当按键释放后 KEY0
为高电平。通过原理图可以确定 KEY0
连接在 UART1_CTS
引脚上。
六、添加设备树
1、确定引脚
通过原理图可以确定 key
使用 UART1_CTS
引脚。
2、查找引脚定义是否冲突
3、添加 pinctrl 子系统相关配置
pinctrl_key: keygrp {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 /* KEY0 */
>;
};
4、添加 gpio 子系统相关配置
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "lq-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
status = "okay";
};
5、添加 interrupts 相关配置
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "lq-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_LEVEL_LOW>; /* 低电平触发 */
status = "okay";
};
主要添加 interrupt-parent
和 interrupts
属性。
6、测试
1、编译设备树
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ make dtbs
CHK include/config/kernel.release
CHK include/generated/uapi/linux/version.h
CHK include/generated/utsrelease.h
make[1]: 'include/generated/mach-types.h' is up to date.
CHK include/generated/bounds.h
CHK include/generated/asm-offsets.h
CALL scripts/checksyscalls.sh
DTC arch/arm/boot/dts/imx6ull-alientek-emmc.dtb
DTC arch/arm/boot/dts/imx6ull-alientek-nand.dtb
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$
2、拷贝编译成功的设备树文件
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ ls /home/onlylove/my/tftp/ -l
total 11528
-rwxrwxr-x 1 onlylove onlylove 5901744 Sep 17 04:04 zImage
-rwxrwxr-x 1 onlylove onlylove 5901752 Aug 20 01:24 zImage.bak
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /home/onlylove/my/tftp
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$ ls /home/onlylove/my/tftp/ -l
total 11568
-rw-rw-r-- 1 onlylove onlylove 39084 Sep 23 21:18 imx6ull-alientek-emmc.dtb
-rwxrwxr-x 1 onlylove onlylove 5901744 Sep 17 04:04 zImage
-rwxrwxr-x 1 onlylove onlylove 5901752 Aug 20 01:24 zImage.bak
onlylove@ubuntu:~/my/linux/linux-imx-4.1.15$
3、启动 linux
查看设备树解析是否成功
/ # cd /proc/
/proc # ls
1 41 65 driver mounts
10 46 66 execdomains mtd
11 47 7 fb net
12 48 8 filesystems pagetypeinfo
13 49 83 fs partitions
14 5 84 interrupts self
15 50 9 iomem softirqs
16 51 95 ioports stat
17 52 asound irq swaps
18 53 buddyinfo kallsyms sys
19 54 bus key-users sysrq-trigger
2 55 cgroups keys sysvipc
20 56 cmdline kmsg thread-self
21 57 consoles kpagecount timer_list
22 58 cpu kpageflags tty
23 59 cpuinfo loadavg uptime
24 6 crypto locks version
3 60 device-tree meminfo vmallocinfo
4 61 devices misc vmstat
40 62 diskstats modules zoneinfo
/proc # cd device-tree/
/sys/firmware/devicetree/base # ls
#address-cells key
#size-cells memory
aliases model
alphaled name
backlight pxp_v4l2
beep regulators
chosen reserved-memory
clocks sii902x-reset
compatible soc
cpus sound
gpioled spi4
interrupt-controller@00a01000
/sys/firmware/devicetree/base # cd key/
/sys/firmware/devicetree/base/key # ls
#address-cells interrupt-parent name status
#size-cells interrupts pinctrl-0
compatible key-gpio pinctrl-names
/sys/firmware/devicetree/base/key # cat compatible
lq-key
/sys/firmware/devicetree/base/key #
/sys/firmware/devicetree/base/key #
七、按键消抖
使用内核定时器进行按键消抖。
Linux 驱动开发 三十四:Linux 内核定时器原理_lqonlylove的博客-CSDN博客_linux内核定时器原理
八、代码逻辑
1、设置按键中断为低电平触发。
2、触发按键中断后,保存按键状态。
3、使用原子变量保存按键状态。
4、在驱动的 read
接口中将按键状态返回给应用程序,并设置按键状态变量值。
九、驱动编写
#include "linux/init.h"
#include "linux/module.h"
#include "linux/kdev_t.h"
#include "linux/fs.h"
#include "linux/cdev.h"
#include "linux/device.h"
#include <linux/uaccess.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#define NEWCHRDEV_MAJOR 0 /* 主设备号(如果为0则让系统自动分配,如果大于0则使用指定设备号) */
#define NEWCHRDEV_MINOR 0 /* 次设备号 */
#define NEWCHRDEV_COUNT 1 /* 设备号个数 */
#define NEWCHRDEV_NAME "key" /* 名子 */
#define TIMEOUT 1000 /* 定时器超时时间 */
typedef struct{
struct device_node *nd; /* 设备节点 */
int gpio; /* key gpio */
int irqnum; /* 中断号 */
struct tasklet_struct tasklet; /* tasklet 变量(中断下半部) */
unsigned long tasklet_data; /* 传递给 tasklet 处理函数的参数 */
struct work_struct work; /* 工作队列 变量(中断下半部) */
struct timer_list timer; /* key 消抖定时器 */
atomic_t keyvalue; /* 按键状态 */
}lqkey_t;
typedef struct{
struct cdev dev; /* cdev 结构体 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */
struct class *class; /* 类 */
struct device *device; /* 设备 */
lqkey_t key_data; /* key 相关数据 */
}newchrdev_t;
newchrdev_t newchrdev;
/*
* @description : 按键IO初始化
* @param : 无
* @return : 无
*/
static int keyio_init(void)
{
int ret = 0;
/***** 处理设备树 *****/
/* 1、获取设备树节点 */
newchrdev.key_data.nd = of_find_node_by_path("/key");
if (newchrdev.key_data.nd== NULL){
printk("key node not find!\r\n");
goto keyio_init_error1;
}else {
printk("beep node find!\r\n");
}
/* 1、获取设备树中的gpio属性 */
newchrdev.key_data.gpio = of_get_named_gpio(newchrdev.key_data.nd, "key-gpio", 0);
if(newchrdev.key_data.gpio < 0) {
printk("can't get key-gpio");
goto keyio_init_error1;
}
printk("key num = %d\r\n", newchrdev.key_data.gpio);
/***** 使用gpio子系统设置引脚 *****/
/* 1、向 gpio 子系统申请 GPIO 管脚 */
ret = gpio_request(newchrdev.key_data.gpio,"key");
if(ret){
printk("can't request key-gpio!\r\n");
goto keyio_init_error1;
}
/* 2、设置key-gpio为输入 */
gpio_direction_input(newchrdev.key_data.gpio);
if(ret < 0) {
printk("can't set key-gpio!\r\n");
goto keyio_init_error2;
}
return 0;
keyio_init_error2:
gpio_free(newchrdev.key_data.gpio);
keyio_init_error1:
return 1;
}
static int keyio_exit(void)
{
/* 1、向gpio子系统请求释放gpio */
gpio_free(newchrdev.key_data.gpio);
return 0;
}
static void key_tasklet(unsigned long data)
{
printk("key_tasklet\r\n");
}
static void key_work(struct work_struct *work)
{
printk("key_work\r\n");
mod_timer(&newchrdev.key_data.timer, jiffies + msecs_to_jiffies(TIMEOUT));
}
/* @description : 中断服务函数
* 定时器用于按键消抖
* @param - irq : 中断号
* @param - dev_id : 设备结构
* @return : 中断执行结果
*/
static irqreturn_t key0_handler(int irq, void *dev_id)
{
printk("key0_handler\r\n");
// tasklet_schedule(&newchrdev.key_data.tasklet);
schedule_work(&newchrdev.key_data.work);
return IRQ_RETVAL(IRQ_HANDLED);
}
/*
* @description : key中断初始化
* @param : 无
* @return : 无
*/
static int keyirq_init(void)
{
int ret = 0;
/***** 使用中断子系统设置 *****/
/* 1、从设备树获取key-gpio对于中断号 */
newchrdev.key_data.irqnum = irq_of_parse_and_map(newchrdev.key_data.nd, 0);
if (!newchrdev.key_data.irqnum){
goto keyirq_init_error1;
}
/* 2、向中断子系统请求中断号 */
ret = request_irq(newchrdev.key_data.irqnum, key0_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,"KEY0",&newchrdev);
if(ret < 0){
printk("can't request irq!\r\n");
goto keyirq_init_error1;
}
/***** 使用中断上下部(tasklet和工作队列选择一个) *****/
/* 1、初始化中断下半部 */
// tasklet_init(&newchrdev.key_data.tasklet,key_tasklet,newchrdev.key_data.tasklet_data);
INIT_WORK(&newchrdev.key_data.work, key_work);
return 0;
keyirq_init_error1:
return 1;
}
static int keyirq_exit(void)
{
/* 1、向中断子系统请求释放中断号 */
free_irq(newchrdev.key_data.irqnum,&newchrdev);
/***** 使用中断上下部(tasklet和工作队列选择一个) *****/
/* 1、销毁中断下半部 */
// tasklet_kill(&newchrdev.key_data.tasklet);
flush_work(&newchrdev.key_data.work);
return 0;
}
void key_timer_function(unsigned long arg)
{
printk("key_timer_function\r\n");
/* 1、设置按键状态(按键按下) */
atomic_set(&newchrdev.key_data.keyvalue,0x01);
}
/*
* @description : key消抖定时器初始化
* @param : 无
* @return : 无
*/
static int keytimer_init(void)
{
/* 1、创建定时器 */
init_timer(&newchrdev.key_data.timer);
newchrdev.key_data.timer.function = key_timer_function;
newchrdev.key_data.timer.expires = jiffies + msecs_to_jiffies(TIMEOUT);
newchrdev.key_data.timer.data = (unsigned long)&newchrdev;
return 0;
}
static int keytimer_exit(void)
{
/* 删除、创建定时器 */
del_timer(&newchrdev.key_data.timer);
return 0;
}
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int newchrdev_open(struct inode *inode, struct file *filp)
{
printk("newchrdev_open!\r\n");
filp->private_data = &newchrdev; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t newchrdev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int ret = 0;
unsigned char keystatus = 0;
// printk("newchrdev_read!\r\n");
keystatus = atomic_read(&newchrdev.key_data.keyvalue);
if(keystatus == 0x01){
ret = copy_to_user(buf, &keystatus, sizeof(keystatus));
atomic_set(&newchrdev.key_data.keyvalue,0xFF);
}else{
goto data_error;
}
return 0;
data_error:
return -EINVAL;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t newchrdev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
printk("newchrdev_write!\r\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int newchrdev_release(struct inode *inode, struct file *filp)
{
printk("newchrdev_release!\r\n");
return 0;
}
static const struct file_operations newchrdevops = {
.owner = THIS_MODULE,
.open = newchrdev_open,
.read = newchrdev_read,
.write = newchrdev_write,
.release = newchrdev_release,
};
/* 驱动入口函数 */
static int __init newchrdev_init(void)
{
/* 驱动入口函数具体内容 */
/* 1、字符设备号分配 */
int ret;
newchrdev.major = NEWCHRDEV_MAJOR;
if(newchrdev.major){
newchrdev.minor = NEWCHRDEV_MINOR;
newchrdev.devid = MKDEV(newchrdev.major, newchrdev.minor);
ret = register_chrdev_region(newchrdev.devid,NEWCHRDEV_COUNT,NEWCHRDEV_NAME);
}else{
ret = alloc_chrdev_region(&newchrdev.devid,0,NEWCHRDEV_COUNT,NEWCHRDEV_NAME);
newchrdev.major = MAJOR(newchrdev.devid);
newchrdev.minor = MINOR(newchrdev.devid);
}
if(ret < 0){
printk("newchrdev xxx_chrdev_region failed!\r\n");
goto newchrdev_chrdev_region_failed;
}
printk("newchrdev major=%d,minor=%d\r\n",newchrdev.major,newchrdev.minor);
/* 2、注册字符设备 */
newchrdev.dev.owner = THIS_MODULE;
cdev_init(&newchrdev.dev,&newchrdevops);
ret = cdev_add(&newchrdev.dev,newchrdev.devid,NEWCHRDEV_COUNT);
if(ret < 0){
printk("newchrdev cdev_add failed!\r\n");
goto newchrdev_cdev_add_failed;
}
/* 3、创建类 */
newchrdev.class = class_create(THIS_MODULE,NEWCHRDEV_NAME);
if(IS_ERR(newchrdev.class)) {
printk("newchrdev class_create failed!\r\n");
goto newchrdev_class_create_failed;
}
/* 4、创建设备 */
newchrdev.device = device_create(newchrdev.class,NULL,newchrdev.devid,NULL,NEWCHRDEV_NAME);
if(IS_ERR(newchrdev.device)){
printk("newchrdev device_create failed!\r\n");
goto neschrdev_device_creat_failed;
}
ret = keyio_init();
if(ret != 0){
goto keyio_init_failed;
}
ret = keyirq_init();
if(ret != 0){
goto keyirq_init_failed;
}
keytimer_init();
/* 初始化按键状态值 */
atomic_set(&newchrdev.key_data.keyvalue,0xFF);
return 0;
keyirq_init_failed:
keyio_exit();
keyio_init_failed:
neschrdev_device_creat_failed:
class_destroy(newchrdev.class);
newchrdev_class_create_failed:
cdev_del(&newchrdev.dev);
newchrdev_cdev_add_failed:
unregister_chrdev_region(newchrdev.devid,NEWCHRDEV_COUNT);
newchrdev_chrdev_region_failed: /* 字符设备号分配失败处理函数(未分配资源,因此不做处理) */
return ret;
}
/* 驱动卸载函数 */
static void __exit newchrdev_exit(void)
{
keytimer_exit();
keyirq_exit();
keyio_exit();
/* 驱动卸载函数具体内容 */
/* 4、删除设备 */
device_destroy(newchrdev.class,newchrdev.devid);
/* 3、删除类 */
class_destroy(newchrdev.class);
/* 2、注销字符设备 */
cdev_del(&newchrdev.dev);
/* 1、释放设备号 */
unregister_chrdev_region(newchrdev.devid,NEWCHRDEV_COUNT);
}
module_init(newchrdev_init);
module_exit(newchrdev_exit);
MODULE_LICENSE("GPL");
十、应用程序编写
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "stdio.h"
int main(int argc, char *argv[])
{
int fd = 0, retvalue = 0;
char readbuf = 0;
if(argc != 2){
printf("Error Usage!\r\n");
return -1;
}
fd = open(argv[1],O_RDWR);
if(fd < 0){
printf("Can't open file %s\r\n", argv[1]);
return -1;
}
printf("sizeof(readbuf) = %d\r\n",sizeof(readbuf));
while(1){
retvalue = read(fd, &readbuf, sizeof(readbuf));
if (retvalue < 0){
}else{
if (readbuf)
printf("key value = %#X\r\n", readbuf);
}
}
close(fd);
return 0;
}
十一、测试
1、加载驱动
/ # ls
beep.ko home lib newchrdev_app tmp
beep_app include linuxrc proc usr
bin key.ko minicom.log root var
dev key_app mnt sbin video
drivers led.ko music share
etc led_app newchrdev.ko sys
/ # insmod key.ko
newchrdev major=248,minor=0
beep node find!
key num = 18
/ # rmmod key.ko
/ # insmod key.ko
newchrdev major=248,minor=0
beep node find!
key num = 18
/ # rmmod key.ko
/ # insmod key.ko
newchrdev major=248,minor=0
beep node find!
key num = 18
/ # rmmod key.ko
2、确定中断是否注册成功
/ # ls
beep.ko home lib newchrdev_app tmp
beep_app include linuxrc proc usr
bin key.ko minicom.log root var
dev key_app mnt sbin video
drivers led.ko music share
etc led_app newchrdev.ko sys
/ # cat /proc/interrupts
CPU0
16: 2578 GPC 55 Level i.MX Timer Tick
18: 0 GPC 33 Level 2010000.ecspi
19: 769 GPC 26 Level 2020000.serial
20: 0 GPC 98 Level sai
21: 0 GPC 50 Level 2034000.asrc
37: 0 gpio-mxc 9 Edge edt-ft5426
47: 0 gpio-mxc 19 Edge 2190000.usdhc cd
196: 0 GPC 4 Level 20cc000.snvs:snvs-powerkey
197: 9778 GPC 120 Level 20b4000.ethernet
198: 0 GPC 121 Level 20b4000.ethernet
199: 0 GPC 80 Level 20bc000.wdog
202: 0 GPC 49 Level imx_thermal
207: 0 GPC 19 Level rtc alarm
213: 0 GPC 2 Level sdma
218: 0 GPC 43 Level 2184000.usb
219: 35 GPC 42 Level 2184200.usb
220: 0 GPC 118 Level 2188000.ethernet
221: 0 GPC 119 Level 2188000.ethernet
222: 34 GPC 22 Level mmc0
223: 64 GPC 23 Level mmc1
224: 1 GPC 100 Level 2198000.adc
225: 0 GPC 36 Level 21a0000.i2c
226: 196 GPC 37 Level 21a4000.i2c
229: 2 GPC 5 Level 21c8000.lcdif
232: 1 GPC 107 Level
233: 0 GPC 28 Level 21ec000.serial
IPI0: 0 CPU wakeup interrupts
IPI1: 0 Timer broadcast interrupts
IPI2: 0 Rescheduling interrupts
IPI3: 0 Function call interrupts
IPI4: 0 Single function call interrupts
IPI5: 0 CPU stop interrupts
IPI6: 0 IRQ work interrupts
IPI7: 0 completion interrupts
Err: 0
/ # insmod key.ko
newchrdev major=248,minor=0
beep node find!
key num = 18
/ # cat /proc/interrupts
CPU0
16: 2718 GPC 55 Level i.MX Timer Tick
18: 0 GPC 33 Level 2010000.ecspi
19: 903 GPC 26 Level 2020000.serial
20: 0 GPC 98 Level sai
21: 0 GPC 50 Level 2034000.asrc
37: 0 gpio-mxc 9 Edge edt-ft5426
46: 0 gpio-mxc 18 Edge KEY0
47: 0 gpio-mxc 19 Edge 2190000.usdhc cd
196: 0 GPC 4 Level 20cc000.snvs:snvs-powerkey
197: 9851 GPC 120 Level 20b4000.ethernet
198: 0 GPC 121 Level 20b4000.ethernet
199: 0 GPC 80 Level 20bc000.wdog
202: 0 GPC 49 Level imx_thermal
207: 0 GPC 19 Level rtc alarm
213: 0 GPC 2 Level sdma
218: 0 GPC 43 Level 2184000.usb
219: 35 GPC 42 Level 2184200.usb
220: 0 GPC 118 Level 2188000.ethernet
221: 0 GPC 119 Level 2188000.ethernet
222: 34 GPC 22 Level mmc0
223: 64 GPC 23 Level mmc1
224: 1 GPC 100 Level 2198000.adc
225: 0 GPC 36 Level 21a0000.i2c
226: 196 GPC 37 Level 21a4000.i2c
229: 2 GPC 5 Level 21c8000.lcdif
232: 1 GPC 107 Level
233: 0 GPC 28 Level 21ec000.serial
IPI0: 0 CPU wakeup interrupts
IPI1: 0 Timer broadcast interrupts
IPI2: 0 Rescheduling interrupts
IPI3: 0 Function call interrupts
IPI4: 0 Single function call interrupts
IPI5: 0 CPU stop interrupts
IPI6: 0 IRQ work interrupts
IPI7: 0 completion interrupts
Err: 0
/ #
通过以上日志可以确定,按键中断加载成功后会出现 46: 0 gpio-mxc 18 Edge KEY0
行。
3、测试驱动程序和应用程序
/ # ls
beep.ko home lib newchrdev_app tmp
beep_app include linuxrc proc usr
bin key.ko minicom.log root var
dev key_app mnt sbin video
drivers led.ko music share
etc led_app newchrdev.ko sys
/ # insmod key.ko
newchrdev major=248,minor=0
beep node find!
key num = 18
/ # ls -l /dev/key
crw-rw---- 1 root 0 248, 0 Jan 1 10:46 /dev/key
/ # rmmod key.ko
/ # ls -l /dev/key
ls: /dev/key: No such file or directory
/ # insmod key.ko
newchrdev major=248,minor=0
beep node find!
key num = 18
/ # ls -l /dev/key
crw-rw---- 1 root 0 248, 0 Jan 1 10:46 /dev/key
/ # ./key_app /dev/key
newchrdev_open!
sizeof(readbuf) = 1
key0_handler
key_work
key0_handler
key_work
key_timer_function
key value = 0X1
key0_handler
key_work
key0_handler
key0_handler
key_work
key_timer_function
key value = 0X1
key0_handler
key_work
key0_handler
key0_handler
key_work
key_timer_function
key value = 0X1
^Cnewchrdev_release!
/ # rmmod key.ko
/ # ls -l /dev/key
ls: /dev/key: No such file or directory
/ #
十二、特别说明
在驱动相关接口中一定要处理异常情况。如果在驱动接口中缺少异常相关处理,会出现未定义现象,极难调试。
记一次失败经历:应用程序读取按键值时,未对 newchrdev_read
进行异常处理,出现以下现象:
- 应用程序中
read
始终返回0
,无法读取到数据。 - 应用程序中
read
始终可以读取到数据,但驱动中未向应用程序传输数据。