• linux串口驱动分析——发送数据


    一、应用程序中write函数到底层驱动历程

      和前文提到的一样,首先先注册串口,使用uart_register_driver函数,依次分别为tty_register_driver,cdev_init函数,找到使用的file_operations,即应用程序与tty架构的统一接口。步骤不再赘述。

    static const struct file_operations tty_fops = {
        .llseek        = no_llseek,
        .read        = tty_read,
        .write        = tty_write,
        .poll        = tty_poll,
        .unlocked_ioctl    = tty_ioctl,
        .compat_ioctl    = tty_compat_ioctl,
        .open        = tty_open,
        .release    = tty_release,
        .fasync        = tty_fasync,
    };

      tty_write函数

    static ssize_t tty_write(struct file *file, const char __user *buf,
                            size_t count, loff_t *ppos)
    {
        struct inode *inode = file->f_path.dentry->d_inode;
        struct tty_struct *tty = file_tty(file);
        struct tty_ldisc *ld;
        ssize_t ret;
        ...
        ret = do_tty_write(ld->ops->write, tty, file, buf, count);
        ...
    }

      这里通过do_tty_write函数调用到了线路规程(ldisc)中的函数,结构名为tty_ldisc_N_TTY。 

    struct tty_ldisc_ops tty_ldisc_N_TTY = {
        .magic           = TTY_LDISC_MAGIC,
        .name            = "n_tty",
        .open            = n_tty_open,
        .close           = n_tty_close,
        .flush_buffer    = n_tty_flush_buffer,
        .chars_in_buffer = n_tty_chars_in_buffer,
        .read            = n_tty_read,
        .write           = n_tty_write,
        .ioctl           = n_tty_ioctl,
        .set_termios     = n_tty_set_termios,
        .poll            = n_tty_poll,
        .receive_buf     = n_tty_receive_buf,
        .write_wakeup    = n_tty_write_wakeup
    };

      n_tty_write函数

    static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
                   const unsigned char *buf, size_t nr)
    {
        const unsigned char *b = buf;
        DECLARE_WAITQUEUE(wait, current);
        int c;
        ssize_t retval = 0;
      ...
      c = tty->ops->write(tty, b, nr);
      ...
    }

      ops为struct tty_operations类型,由上文可知该结构名为

    static const struct tty_operations uart_ops = {
        ...
        .write        = uart_write,
        ...
    };

      uart_write函数

    static int uart_write(struct tty_struct *tty,
                        const unsigned char *buf, int count)
    {
        struct uart_state *state = tty->driver_data;
        struct uart_port *port;
        struct circ_buf *circ;
        unsigned long flags;
        int c, ret = 0;
      ...
      uart_start(tty);
      ...
    }

      uart_start函数中又调用了__uart_start函数

    static void __uart_start(struct tty_struct *tty)
    {
        struct uart_state *state = tty->driver_data;
        struct uart_port *port = state->uart_port;
    
        if (!uart_circ_empty(&state->xmit) && state->xmit.buf &&
            !tty->stopped && !tty->hw_stopped)
            port->ops->start_tx(port);
    }

      这里的port就是uart_port类型的了,终于到达底层驱动了,好累。。又是这个数组,同样的函数操作集

      

    static struct s3c24xx_uart_port s3c24xx_serial_ports[CONFIG_SERIAL_SAMSUNG_UARTS] = {
        [0] = {
            .port = {
                .lock        = __SPIN_LOCK_UNLOCKED(s3c24xx_serial_ports[0].port.lock),
                .iotype        = UPIO_MEM,
                .irq        = IRQ_S3CUART_RX0,
                .uartclk    = 0,
                .fifosize    = 16,
                .ops        = &s3c24xx_serial_ops,
                .flags        = UPF_BOOT_AUTOCONF,
                .line        = 0,
            }
        },
      ...
    }

      所以在底层驱动与之对应的就是s3c24xx_serial_start_tx这个函数。层层追溯下来,最终与应用程序中的write函数千里相会。。。

    static void s3c24xx_serial_start_tx(struct uart_port *port)
    {
        struct s3c24xx_uart_port *ourport = to_ourport(port);
    
        if (!tx_enabled(port)) {
            if (port->flags & UPF_CONS_FLOW)
                s3c24xx_serial_rx_disable(port);
    
            enable_irq(ourport->tx_irq);
            tx_enabled(port) = 1;
        }
    }

      但是要让各位看官失望了这个函数很简单,功能上来说就是如果没有打开发送使能就去打开。那么串口驱动又是在什么地方去完成发送数据相关的操作呢?

    二、底层驱动发送中断处理函数

      看到这个标题大家应该就明白了。start_tx函数只是用来打开发送中断,而真正进行发送操作的却是在中断处理函数中。在驱动文件中查找注册中断函数request_irq,最终找到了发送中断处理函数s3c24xx_serial_tx_chars,我们分段来看

    struct s3c24xx_uart_port *ourport = id;
        struct uart_port *port = &ourport->port;
        struct circ_buf *xmit = &port->state->xmit;
        int count = 256;
    
        if (port->x_char) {
            wr_regb(port, S3C2410_UTXH, port->x_char);
            port->icount.tx++;
            port->x_char = 0;
            goto out;
        }

      首先定义了一些结构和变量。接下来一个if分支,判断是否有需要发送的x_char。x_char用于两个硬件之间的通信,若接收方需要接收数据则向发送方发送x_on,否则发送x_off。只需将该变量写入寄存器UTXH中即可发送。当然在发送完毕后要将其置零。

    if (uart_circ_empty(xmit) || uart_tx_stopped(port)) {
            s3c24xx_serial_stop_tx(port);
            goto out;
        }

      检查循环缓冲是否为空或停止了串口发送,满足任一条件即关闭发送使能。

    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++;
        }

      这段就是名副其实的发送函数了。首先看循环条件:其一,循环缓冲不能为空。这是自然,因为发送的数据是从循环缓冲中取得,空了还怎么发。其二:count自减结果不能小于0。在前面定义了count初值为256,也就是说一次中断最多能发送256个数据量。这是linux的一种保护机制,如果说待发送的数据过多,那么就会长时间停留在这个中断中,linux不能做其他事情,显然这是不合理的。进入循环体,又是一个判断分支。UFSTAT寄存器是记录发送FIFO状态的寄存器,ourport->info->tx_fifofull在linux内核中查出它的值是(1<<14)

      

    也就是说,当发送FIFO中数据存满时,也要退出循环。接下来就是将循环缓冲中的数据写入发送寄存器UTXH,在调整tail指针的位置。注意由于tail和head的值不能超过循环缓冲的空间,所以超出时将其置零。

    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;

      第一个分支,如果循环缓冲中的数据量小于WAKEUP_CHARS(256),则唤醒之前向循环缓冲中写入数据的函数。第二个分支,如果循环缓冲为空,则停止发送。

      这就是整个发送数据的函数。

    三、补充

      1、循环缓冲

      循环缓冲是linux内核定义的一种数据结构

    struct circ_buf {
        char *buf;
        int head;
        int tail;
    };

      工作机制:

        收入n个数据,head=head+n,发送n个数据,tail=tail+n,相当于一个循环队列,先进先出。  

      write函数中的数据并不是直接写入UTXH寄存器中,而是先写入循环缓冲。具体实现是在uart_write函数中。

      memcpy(circ->buf + circ->head, buf, c);
        circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);

      这两句代码将buf中的数据写入循环缓冲中,并修改了head的位置。

      2、发送FIFO

      为了处理器的工作效率,发送出的数据也不是直接到达相应的硬件,而是存入发送FIFO中,当处理器闲置时在进行发送。

    至此,linux串口驱动程序发送数据的实现已分析完毕。如果有疑问或错误,欢迎指出。

  • 相关阅读:
    《那些年啊,那些事——一个程序员的奋斗史》——30
    《那些年啊,那些事——一个程序员的奋斗史》——33
    《那些年啊,那些事——一个程序员的奋斗史》——31
    《那些年啊,那些事——一个程序员的奋斗史》——31
    《那些年啊,那些事——一个程序员的奋斗史》——32
    《那些年啊,那些事——一个程序员的奋斗史》——32
    《那些年啊,那些事——一个程序员的奋斗史》——32
    《那些年啊,那些事——一个程序员的奋斗史》——34
    《那些年啊,那些事——一个程序员的奋斗史》——33
    《那些年啊,那些事——一个程序员的奋斗史》——31
  • 原文地址:https://www.cnblogs.com/51qianrushi/p/4324845.html
Copyright © 2020-2023  润新知