串口驱动分析之读写数据流

来源:互联网 发布:如何改淘宝的主营类目 编辑:程序博客网 时间:2024/06/03 21:56

数据读取数据流:

 读数据流分为两段,第一段为用户空间从环形缓冲read_buf中获取数据;第二段为

硬件设备获取数据写到环形缓冲read_buf中。

一:

 (1) tty_io.c

static ssize_t tty_read(struct file *file, char __user *buf, size_t count,loff_t *ppos)

{

int i;

struct tty_struct *tty;

struct inode *inode;

struct tty_ldisc *ld;

tty = (struct tty_struct *)file->private_data;

inode = file->f_path.dentry->d_inode;

if (tty_paranoia_check(tty, inode, "tty_read"))

return -EIO;

if (!tty || (test_bit(TTY_IO_ERROR, &tty->flags)))

return -EIO;

/* We want to wait for the line discipline to sort out in this

   situation */

ld = tty_ldisc_ref_wait(tty);//获取线路规程操作函数

if (ld->ops->read)

i = (ld->ops->read)(tty, file, buf, count);//将环形缓冲区read_buf的数据读取到用户空间

else

i = -EIO;

tty_ldisc_deref(ld);

if (i > 0)

inode->i_atime = current_fs_time(inode->i_sb);

return i;

}

(2) N_tty.c

 函数n_tty_read即为(1)中调用函数ld->ops->read  。

static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,

 unsigned char __user *buf, size_t nr)

{

unsigned char __user *b = buf;

DECLARE_WAITQUEUE(wait, current);

int c;

int minimum, time;

ssize_t retval = 0;

ssize_t size;

long timeout;

unsigned long flags;

int packet;

do_it_again:

BUG_ON(!tty->read_buf);

c = job_control(tty, file);

if (c < 0)

return c;

minimum = time = 0;

timeout = MAX_SCHEDULE_TIMEOUT;

//

//函数n_tty_receive_buf将数据接收缓存链中接收到的数据读到tty->read_buf中,

//当tty->read_buf中的数据量达到tty->read_cnt >= tty->minimum_to_wake时

//调用函数kill_fasync(&tty->fasync, SIGIO, POLL_IN);向用户空间发出异步通知。

//wake_up_interruptible(&tty->read_wait);唤醒用户空间的读进程。

if (!tty->icanon) {

time = (HZ / 10) * TIME_CHAR(tty);

minimum = MIN_CHAR(tty);

if (minimum) {

if (time)

tty->minimum_to_wake = 1;

else if (!waitqueue_active(&tty->read_wait) ||

 (tty->minimum_to_wake > minimum))

tty->minimum_to_wake = minimum;

} else {

timeout = 0;

if (time) {

timeout = time;

time = 0;

}

tty->minimum_to_wake = minimum = 1;

}

}

/*

 * Internal serialization of reads.

 */

if (file->f_flags & O_NONBLOCK) {

if (!mutex_trylock(&tty->atomic_read_lock))

return -EAGAIN;

} else {

if (mutex_lock_interruptible(&tty->atomic_read_lock))

return -ERESTARTSYS;

}

packet = tty->packet;

add_wait_queue(&tty->read_wait, &wait);

while (nr) {

/* First test for status change. */

if (packet && tty->link->ctrl_status) {

unsigned char cs;

if (b != buf)

break;

spin_lock_irqsave(&tty->link->ctrl_lock, flags);

cs = tty->link->ctrl_status;

tty->link->ctrl_status = 0;

spin_unlock_irqrestore(&tty->link->ctrl_lock, flags);

if (tty_put_user(tty, cs, b++)) {

retval = -EFAULT;

b--;

break;

}

nr--;

break;

}

/* This statement must be first before checking for input

   so that any interrupt will set the state back to

   TASK_RUNNING. */

set_current_state(TASK_INTERRUPTIBLE);

if (((minimum - (b - buf)) < tty->minimum_to_wake) &&

    ((minimum - (b - buf)) >= 1))

tty->minimum_to_wake = (minimum - (b - buf));

if (!input_available_p(tty, 0)) {

if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {

retval = -EIO;

break;

}

if (tty_hung_up_p(file))

break;

if (!timeout)

break;

if (file->f_flags & O_NONBLOCK) {

retval = -EAGAIN;

break;

}

if (signal_pending(current)) {

retval = -ERESTARTSYS;

break;

}

/* FIXME: does n_tty_set_room need locking ? */

n_tty_set_room(tty);

timeout = schedule_timeout(timeout);

continue;

}

__set_current_state(TASK_RUNNING);

/* Deal with packet mode. */

if (packet && b == buf) {

if (tty_put_user(tty, TIOCPKT_DATA, b++)) {

retval = -EFAULT;

b--;

break;

}

nr--;

}

if (tty->icanon) {//如果开启了tty->icanon则数据需要逐个获取

/* N.B. avoid overrun if nr == 0 */

while (nr && tty->read_cnt) {

int eol;

//eol为0表示对应数据是一个正确的数据。

eol = test_and_clear_bit(tty->read_tail,

tty->read_flags);

c = tty->read_buf[tty->read_tail];//从数据缓存中逐个数据读出

spin_lock_irqsave(&tty->read_lock, flags);

tty->read_tail = ((tty->read_tail+1) &

  (N_TTY_BUF_SIZE-1));//避免环形缓存区回绕

tty->read_cnt--;

if (eol) {

/* this test should be redundant:

 * we shouldn't be reading data if

 * canon_data is 0

 */

if (--tty->canon_data < 0)

tty->canon_data = 0;

}

spin_unlock_irqrestore(&tty->read_lock, flags);

if (!eol || (c != __DISABLED_CHAR)) {

if (tty_put_user(tty, c, b++)) {

retval = -EFAULT;

b--;

break;

}

nr--;

}

if (eol) {

tty_audit_push(tty);

break;

}

}

if (retval)

break;

} else {

int uncopied;

/* The copy function takes the read lock and handles

   locking internally for this case */

/*

 数据读取缓存read_buf(0~N_TTY_BUF_SIZE)为一环形缓冲区。tty->read_tail,

 tty->read_tail指向第一个未被读取的数据, tty->read_cnt缓存中的数据,tty->read_head指向

 第一个未被占用的空间。由于是环形缓存tty->read_cnt不一定等于tty->read_head - tty->read_tail。

 tty->read_head可能小于tty->read_tail所以可能有以下关系:

 tty->read_cnt = N_TTY_BUF_SIZE -  tty->read_tail + tty->read_head。

 

 所以将read_buf中的值考到用户空间需要考两次,*nr的值可能大于N_TTY_BUF_SIZE -  tty->read_tail

 而小于tty->read_cnt。拷数据时是从tty->read_tail开始,第一次考取N_TTY_BUF_SIZE -  tty->read_tail,

 第二次在read_buf的开始位置到tty->read_head之间获取还需的数据。

*/    

uncopied = copy_from_read_buf(tty, &b, &nr);//将多个数据读到用户空间

uncopied += copy_from_read_buf(tty, &b, &nr);

if (uncopied) {

retval = -EFAULT;

break;

}

}

/* If there is enough space in the read buffer now, let the

 * low-level driver know. We use n_tty_chars_in_buffer() to

 * check the buffer, as it now knows about canonical mode.

 * Otherwise, if the driver is throttled and the line is

 * longer than TTY_THRESHOLD_UNTHROTTLE in canonical mode,

 * we won't get any more characters.

 */

if (n_tty_chars_in_buffer(tty) <= TTY_THRESHOLD_UNTHROTTLE) {

n_tty_set_room(tty);

check_unthrottle(tty);

}

if (b - buf >= minimum)

break;

if (time)

timeout = time;

}

mutex_unlock(&tty->atomic_read_lock);

remove_wait_queue(&tty->read_wait, &wait);

if (!waitqueue_active(&tty->read_wait))

tty->minimum_to_wake = minimum;

__set_current_state(TASK_RUNNING);

size = b - buf;

if (size) {

retval = size;

if (nr)

clear_bit(TTY_PUSH, &tty->flags);

} else if (test_and_clear_bit(TTY_PUSH, &tty->flags))

 goto do_it_again;

n_tty_set_room(tty);//将环形缓冲区中的剩余空间量存于tty->receive_room。

return retval;

}

二:

 (1)samsung.c 

数据接收中断函数将接收到的数据通过函数uart_insert_char放到数据接收缓存链:

static irqreturn_t

s3c24xx_serial_rx_chars(int irq, void *dev_id)

{

struct s3c24xx_uart_port *ourport = dev_id;

struct uart_port *port = &ourport->port;

struct tty_struct *tty = port->info->port.tty;

unsigned int ufcon, ch, flag, ufstat, uerstat;

int max_count = 64;//一次中断接收的最大数据量

while (max_count-- > 0) {

ufcon = rd_regl(port, S3C2410_UFCON);

ufstat = rd_regl(port, S3C2410_UFSTAT);

if (s3c24xx_serial_rx_fifocnt(ourport, ufstat) == 0)//看接收fifo中是否有数据

break;

uerstat = rd_regl(port, S3C2410_UERSTAT);

ch = rd_regb(port, S3C2410_URXH);//读取一个字符

if (port->flags & UPF_CONS_FLOW) {//自动流控制

int txe = s3c24xx_serial_txempty_nofifo(port);

//如果发送器不为空则清零接收标志continue,下一次循环进入else如果发送器为空

//复位接收fifo后置接收标志然后goto out;

if (rx_enabled(port)) {

if (!txe) {

rx_enabled(port) = 0;

continue;

}

} else {

if (txe) {

ufcon |= S3C2410_UFCON_RESETRX;

wr_regl(port, S3C2410_UFCON, ufcon);

rx_enabled(port) = 1;

goto out;

}

continue;

}

}

/* insert the character into the buffer */

flag = TTY_NORMAL;//接收到的是正确的数据

port->icount.rx++;//接收数据个数加

if (unlikely(uerstat & S3C2410_UERSTAT_ANY)) {//判断是否有错误标志被置位,并做相应处理

dbg("rxerr: port ch=0x%02x, rxs=0x%08x\n",

    ch, uerstat);

/* check for break */

if (uerstat & S3C2410_UERSTAT_BREAK) {

dbg("break!\n");

port->icount.brk++;

if (uart_handle_break(port))

    goto ignore_char;

}

if (uerstat & S3C2410_UERSTAT_FRAME)

port->icount.frame++;

if (uerstat & S3C2410_UERSTAT_OVERRUN)

port->icount.overrun++;

uerstat &= port->read_status_mask;

if (uerstat & S3C2410_UERSTAT_BREAK)

flag = TTY_BREAK;

else if (uerstat & S3C2410_UERSTAT_PARITY)

flag = TTY_PARITY;

else if (uerstat & (S3C2410_UERSTAT_FRAME |

    S3C2410_UERSTAT_OVERRUN))

flag = TTY_FRAME;

}

if (uart_handle_sysrq_char(port, ch))

goto ignore_char;

/*

struct tty_buffer {

struct tty_buffer *next;//连接指针链表

char *char_buf_ptr;//指向数据缓存

unsigned char *flag_buf_ptr;//指向存放标志的缓存

int used;//该段缓存第一个未被占用的数据位置,该段缓存中数据被读取该值不减

int size;//该段数据缓存的大小

int commit;//该段缓存中最后一次写入时,数据缓存中的数据量,数据被读取时该值不减

int read;//指向第一个未被读取的数据,数据被读取其值加。commit - read表示缓存中的数据量

// Data points here 

//缓存被分配时数据存放首地址放于此处,让char_buf_ptr指向这里,

//flag_buf_ptr指向data[0] + (size + 0xFF)处。

unsigned long data[0];

};

将接收到的标志flag放入tty->buf.tail->flag_buf_ptr[tb->used](flag表明数据是否正常在数据读取时查看),

将数据ch放入tty->buf.tail->char_buf_ptr[tb->used++].

接收数据存放的缓存链由结构体struct tty_bufhead来管理

struct tty_bufhead {

struct delayed_work work;

spinlock_t lock;

struct tty_buffer *head; // Queue head 

struct tty_buffer *tail; // Active buffer 

struct tty_buffer *free;//  Free queue head 

int memory_used; //Buffer space used excluding

free queue 

};

head指向缓存链的头,在数据读取时从此开始;tail指向缓存链的尾,当数据写入时写到此处;

free指向已被读取数据的缓存。每次存入数据时放到tail指向的缓存,

如果该段缓存已用完将free指向的缓存链中的一段缓存添加到tail。如果

free指向的缓存链没有足够大的缓存或没有缓存,则分配二倍(size + 0xFF)(size为要存入数据的大小)

的缓存分别给tty->buf.tail->flag_buf_ptr和tty->buf.tail->char_buf_ptr,然后挂到tail。

*/

uart_insert_char(port, uerstat, S3C2410_UERSTAT_OVERRUN,

 ch, flag);

 ignore_char:

continue;

}

/*

将tail指向的数据缓存已被占用的值used赋给commit,tty->buf.tail->commit = tty->buf.tail->used;

调用函数flush_to_ldisc。

flush_to_ldisc:

     如果head指向的数据缓存的数据已被完全读取,则将此缓存挂到free上。调用函数disc->ops->receive_buf

disc->ops->receive_buf:

 将数据拷贝到读数据缓存器中tty->read_buf,在函数 ld->ops->read 拷贝到用户空间copy_from_read_buf(tty, &b, &nr);

*/

tty_flip_buffer_push(tty);

 out:

return IRQ_HANDLED;

}

(2)tty_buffer.c

void tty_flip_buffer_push(struct tty_struct *tty)

{

unsigned long flags;

spin_lock_irqsave(&tty->buf.lock, flags);

//将tail指向的数据缓存已被占用的值used赋给commit

if (tty->buf.tail != NULL)

tty->buf.tail->commit = tty->buf.tail->used;

spin_unlock_irqrestore(&tty->buf.lock, flags);

// 如果head指向的数据缓存的数据已被完全读取,则将此缓存挂到free上。调用函数//disc->ops->receive_buf将接收数据缓存链的数据拷贝到环形缓冲tty->read_buf

if (tty->low_latency)

flush_to_ldisc(&tty->buf.work.work);

else

schedule_delayed_work(&tty->buf.work, 1);

}

(3)tty_buffer.c

static void flush_to_ldisc(struct work_struct *work)

{

struct tty_struct *tty =

container_of(work, struct tty_struct, buf.work.work);

unsigned long  flags;

struct tty_ldisc *disc;

struct tty_buffer *tbuf, *head;

char *char_buf;

unsigned char *flag_buf;

disc = tty_ldisc_ref(tty);//获取线路规程操作函数并增加引用计数

if (disc == NULL) /*  !TTY_LDISC */

return;

spin_lock_irqsave(&tty->buf.lock, flags);

/* So we know a flush is running */

set_bit(TTY_FLUSHING, &tty->flags);

head = tty->buf.head;//获取接收数据缓存链的头

if (head != NULL) {

tty->buf.head = NULL;

for (;;) {

int count = head->commit - head->read;

if (!count) {//如果该段缓存中的数据已被完全读出,则将这段缓存挂到free上。

if (head->next == NULL)

break;

tbuf = head;

head = head->next;

tty_buffer_free(tty, tbuf);

continue;

}

/* Ldisc or user is trying to flush the buffers

   we are feeding to the ldisc, stop feeding the

   line discipline as we want to empty the queue */

if (test_bit(TTY_FLUSHPENDING, &tty->flags))

break;

if (!tty->receive_room) {

schedule_delayed_work(&tty->buf.work, 1);

break;

}

if (count > tty->receive_room)

count = tty->receive_room;

char_buf = head->char_buf_ptr + head->read;//确定数据读取的起始位置

flag_buf = head->flag_buf_ptr + head->read;//确定标志读取的起始位置

head->read += count;

spin_unlock_irqrestore(&tty->buf.lock, flags);

disc->ops->receive_buf(tty, char_buf,

flag_buf, count);//将数据拷贝到读数据缓存器中tty->read_buf,

spin_lock_irqsave(&tty->buf.lock, flags);

}

/* Restore the queue head */

tty->buf.head = head;

}

/* We may have a deferred request to flush the input buffer,

   if so pull the chain under the lock and empty the queue */

if (test_bit(TTY_FLUSHPENDING, &tty->flags)) {

__tty_buffer_flush(tty);

clear_bit(TTY_FLUSHPENDING, &tty->flags);

wake_up(&tty->read_wait);

}

clear_bit(TTY_FLUSHING, &tty->flags);

spin_unlock_irqrestore(&tty->buf.lock, flags);

tty_ldisc_deref(disc);//减少线路规程操作函数的引用计数

}

(4)N_tty.c

static void n_tty_receive_buf(struct tty_struct *tty, const unsigned char *cp,

      char *fp, int count)

{

const unsigned char *p;

char *f, flags = TTY_NORMAL;

int i;

char buf[64];

unsigned long cpuflags;

if (!tty->read_buf)//如果数据读取缓存为空则返回

return;

if (tty->real_raw) {//批量读取

spin_lock_irqsave(&tty->read_lock, cpuflags);

i = min(N_TTY_BUF_SIZE - tty->read_cnt,

N_TTY_BUF_SIZE - tty->read_head);

i = min(count, i);

//将数据接收缓存链中接收到的数据拷贝到环形数据接收缓存。

memcpy(tty->read_buf + tty->read_head, cp, i);

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

cp += i;

count -= i;

i = min(N_TTY_BUF_SIZE - tty->read_cnt,

N_TTY_BUF_SIZE - tty->read_head);

i = min(count, i);

memcpy(tty->read_buf + tty->read_head, cp, i);

tty->read_head = (tty->read_head + i) & (N_TTY_BUF_SIZE-1);

tty->read_cnt += i;

spin_unlock_irqrestore(&tty->read_lock, cpuflags);

} else {

//逐个数据读取并判断每个数据对应的标志位

for (i = count, p = cp, f = fp; i; i--, p++) {

if (f)

flags = *f++;

switch (flags) {

case TTY_NORMAL:

n_tty_receive_char(tty, *p);

break;

case TTY_BREAK:

n_tty_receive_break(tty);

break;

case TTY_PARITY:

case TTY_FRAME:

n_tty_receive_parity_error(tty, *p);

break;

case TTY_OVERRUN:

n_tty_receive_overrun(tty);

break;

default:

printk(KERN_ERR "%s: unknown flag %d\n",

       tty_name(tty, buf), flags);

break;

}

}

if (tty->ops->flush_chars)

tty->ops->flush_chars(tty);

}

//将环形数据缓存tty->read_cnt还剩余的空间量存到tty->receive_room

n_tty_set_room(tty);

//如果tty->icanon关闭,即数据批量读取到用户空间且接收到的数据量达到设定阀值,

//则发出异步通知并唤醒读进程。

if (!tty->icanon && (tty->read_cnt >= tty->minimum_to_wake)) {

kill_fasync(&tty->fasync, SIGIO, POLL_IN);

if (waitqueue_active(&tty->read_wait))

wake_up_interruptible(&tty->read_wait);

}

/*

 * Check the remaining room for the input canonicalization

 * mode.  We don't want to throttle the driver if we're in

 * canonical mode and don't have a newline yet!

 */

if (tty->receive_room < TTY_THRESHOLD_THROTTLE)

tty_throttle(tty);//如果唤醒缓冲剩余空间达到阀值,则向设备发出通知,停止数据发送。

}

数据写入数据流:

(1)tty_io.c

 static ssize_t tty_write(struct file *file, const char __user *buf,

size_t count, loff_t *ppos)

{

struct tty_struct *tty;

struct inode *inode = file->f_path.dentry->d_inode;

ssize_t ret;

struct tty_ldisc *ld;

tty = (struct tty_struct *)file->private_data;

if (tty_paranoia_check(tty, inode, "tty_write"))

return -EIO;

if (!tty || !tty->ops->write ||

(test_bit(TTY_IO_ERROR, &tty->flags)))

return -EIO;

/* Short term debug to catch buggy drivers */

if (tty->ops->write_room == NULL)

printk(KERN_ERR "tty driver %s lacks a write_room method.\n",

tty->driver->name);

ld = tty_ldisc_ref_wait(tty);//获取线路规程操作函数结构体

if (!ld->ops->write)

ret = -EIO;

else

//将要写入的数据考到写入缓存,调用线路规程写函数ld->ops->write,在该函数中 再调用函数

//tty->ops->write将写入缓存中的数据考到uart_info.xmit中,再经数据发送中断处理函数写入发送器。

ret = do_tty_write(ld->ops->write, tty, file, buf, count);

tty_ldisc_deref(ld);

return ret;

}

(2)tty_io.c

static inline ssize_t do_tty_write(

ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t),

struct tty_struct *tty,

struct file *file,

const char __user *buf,

size_t count)

{

ssize_t ret, written = 0;

unsigned int chunk;

ret = tty_write_lock(tty, file->f_flags & O_NDELAY);

if (ret < 0)

return ret;

/*

 * We chunk up writes into a temporary buffer. This

 * simplifies low-level drivers immensely, since they

 * don't have locking issues and user mode accesses.

 *

 * But if TTY_NO_WRITE_SPLIT is set, we should use a

 * big chunk-size..

 *

 * The default chunk-size is 2kB, because the NTTY

 * layer has problems with bigger chunks. It will

 * claim to be able to handle more characters than

 * it actually does.

 *

 * FIXME: This can probably go away now except that 64K chunks

 * are too likely to fail unless switched to vmalloc...

 */

chunk = 2048;

if (test_bit(TTY_NO_WRITE_SPLIT, &tty->flags))

chunk = 65536;

if (count < chunk)

chunk = count;

/* write_buf/write_cnt is protected by the atomic_write_lock mutex */

if (tty->write_cnt < chunk) {//如果数据写入缓存空间大小小于要写入数据,则重新分配内存并释放原有内存

unsigned char *buf_chunk;

if (chunk < 1024)

chunk = 1024;

buf_chunk = kmalloc(chunk, GFP_KERNEL);

if (!buf_chunk) {

ret = -ENOMEM;

goto out;

}

kfree(tty->write_buf);

tty->write_cnt = chunk;

tty->write_buf = buf_chunk;

}

/* Do the write .. */

for (;;) {

size_t size = count;

if (size > chunk)

size = chunk;

ret = -EFAULT;

if (copy_from_user(tty->write_buf, buf, size))//将要写入的数据考到写入缓存

break;

//调用线路规程写函数ld->ops->write,在该函数中 再调用函数tty->ops->write将写入缓存中的数据

//考到uart_info.xmit中,再经数据发送中断处理函数写入发送器。

ret = write(tty, file, tty->write_buf, size);

if (ret <= 0)

break;

written += ret;

buf += ret;

count -= ret;

if (!count)

break;

ret = -ERESTARTSYS;

if (signal_pending(current))

break;

cond_resched();

}

if (written) {

struct inode *inode = file->f_path.dentry->d_inode;

inode->i_mtime = current_fs_time(inode->i_sb);

ret = written;

}

out:

tty_write_unlock(tty);

return ret;

}

(3)samsung.c 

   //数据发送中断处理函数。

static irqreturn_t s3c24xx_serial_tx_chars(int irq, void *id)

{

struct s3c24xx_uart_port *ourport = id;

struct uart_port *port = &ourport->port;

struct circ_buf *xmit = &port->info->xmit;

int count = 256;

//当存放接收数据的缓存满或空时xof/fxon,通知设备不要发送更多数据了,或通知设备开始发送数据

if (port->x_char) {//发送特殊字符xon/xoff

wr_regb(port, S3C2410_UTXH, port->x_char);

port->icount.tx++;//数据发送计数加

port->x_char = 0;

goto out;

}

/* if there isnt anything more to transmit, or the uart is now

 * stopped, disable the uart and exit

*/

if (uart_circ_empty(xmit) || uart_tx_stopped(port)) {

s3c24xx_serial_stop_tx(port);//如果存放发送数据的缓存为空

goto out;

}

/* try and drain the buffer... */

while (!uart_circ_empty(xmit) && count-- > 0) {//发送完指定的数据量

if (rd_regl(port, S3C2410_UFSTAT) & ourport->info->tx_fifofull)

break;

wr_regb(port, S3C2410_UTXH, xmit->buf[xmit->tail]);

xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);

port->icount.tx++;

}

if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)

uart_write_wakeup(port);//如果数据缓存中的数据量小于设定值则唤醒写进程

if (uart_circ_empty(xmit))//如果数据缓存中的数据已被全部发送,则停止

s3c24xx_serial_stop_tx(port);

 out:

return IRQ_HANDLED;

}

原创粉丝点击