串行驱动层

作者:Greg Kroah-Hartman

在上一期和上上期“Driving Me Nuts”专栏文章 [LJ 2002 年 8 月刊和 2002 年 10 月刊] 中,我们介绍了 tty 层,解释了如何创建一个最小的 tty 驱动程序。我们还解释了一些不同的 ioctl 以及如何解释 termios 结构。如果您需要为您的嵌入式系统(例如串行端口)实现一个新的 tty 类型设备,那么这些文章是一个很好的开始。对于每个新设计的系统,硬件工程师总是喜欢将串行端口放置在不同的地址,或使用不同的 UART,或者有时干脆忘记串行端口而使用 USB 端口。因此,大多数开发人员都希望为他们的新设备创建一个全新的 tty 驱动程序,以便硬件在 Linux 上正常工作。幸运的是,tty 层之上还有一些层,可以帮助缓冲其复杂性,并为开发人员提供许多串行驱动程序所需的通用功能,并且更适合 UART 或 USB 模型。这些层是串行驱动层和 USB 转串行驱动层。我们在本文中介绍串行驱动层,并在以后的文章中介绍 USB 转串行层。

串行混乱

如果您查看 2.2 和 2.4 内核(位于 drivers/char/serial.c)中通用 PC 串行驱动程序的代码,您会看到一个非常复杂的驱动程序,其中包含许多 #ifdef 行,具体取决于您使用的硬件类型。为了为不符合典型 PC UART 设备(例如 serial_amba.c 和 serial_21285.c 驱动程序)的设备提供串行支持,此文件已被多次复制。值得庆幸的是,在 ARM Linux 维护者 Russell King 的领导下,许多开发人员将串行驱动程序重组为通用串行核心和许多较小的、特定于硬件的驱动程序。该代码大约在 2.5.28 版本左右出现在主内核树中。本文中的示例来自 2.5.35 版本,因此请检查您使用的内核版本是否发生了更改。

注册串行驱动程序

串行层要求您的驱动程序执行两件事:将驱动程序本身注册到串行核心,然后注册在系统中找到的各个串行端口(通过 PCI 枚举或某种设备发现机制)。要注册您的驱动程序,请使用指向 struct uart_driver 结构的指针调用 uart_register_driver()。此函数接受 uart_driver 结构中提供的信息,并基于此设置 tty 层。

串行驱动程序需要在 uart_driver 结构中提供的字段如下:

struct module           *owner;
const char              *driver_name;
const char              *dev_name;
int                      major;
int                      minor;
int                      nr;
struct console          *cons;

owner 字段是指向拥有串行驱动程序的模块的指针。这通常设置为宏 THIS_MODULE。

driver_name 字段是指向描述此驱动程序的字符串的指针,该字符串通常与 dev_name 字段相同。但是,在描述 dev_name 字段时,需要考虑 devfs,如果选择了 devfs,则必须在此字段的末尾添加字符“%d”。这是因为 devfs 创建设备节点的方式。例如,amba.c 驱动程序将 driver_name 和 dev_name 字段设置为如下:

        .driver_name            = "ttyAM",
#ifdef CONFIG_DEVFS_FS
        .dev_name               = "ttyAM%d",
#else
        .dev_name               = "ttyAM",
#endif

major 和 minor 字段分别指定驱动程序的主设备号和起始次设备号。

nr 字段指定此驱动程序支持的最大串行端口数。

cons 字段是指向 struct console 结构的指针,如果此驱动程序可以支持串行控制台,则使用该结构。如果驱动程序不支持串行控制台模式,则此字段应设置为 NULL。

注册串行端口

现在串行驱动程序已在串行驱动层注册,每个串行端口都需要通过调用 uart_add_one_port() 单独注册。此函数接受指向传递给 uart_register_driver 的原始 uart_driver 结构的指针和指向 uart_port 结构的指针。uart_port 结构定义如下:

struct uart_port {
  spinlock_t       lock;      /* port lock */
  unsigned int     iobase;    /* in/out[bwl] */
  char             *membase;  /* read/write[bwl] */
  unsigned int     irq;       /* irq number */
  unsigned int     uartclk;   /* base uart clock */
  unsigned char    fifosize;  /* tx fifo size */
  unsigned char    x_char;    /* xon/xoff char */
  unsigned char    regshift;  /* reg offset shift */
  unsigned char    iotype;    /* io access style */
  unsigned int     read_status_mask;
                              /* driver specific */
  unsigned int     ignore_status_mask;
                              /* driver specific */
  struct uart_info *info;
                        /* pointer to parent info */
  struct uart_icount icount; /* statistics */
  struct console   *cons;
                      /* struct console, if any */
#ifdef CONFIG_SERIAL_CORE_CONSOLE
  unsigned long sysrq;       /* sysrq timeout */
#endif
  unsigned int     flags;
  unsigned int     mctrl;
                 /* current modem ctrl settings */
  unsigned int     timeout;
                 /* character-based timeout */
  unsigned int     type;      /* port type */
  struct uart_ops  *ops;
  unsigned int     line;      /* port index */
  unsigned long    mapbase;   /* for ioremap */
  unsigned char    hub6;
           /* this should be in the 8250 driver */
  unsigned char unused[3];
    };

这些字段中的大多数在各个串行驱动程序的操作期间使用,以定义特定端口如何连接到处理器(通过 hub6、iobase、membase、mapbase 和 iotype 变量)。

此结构中更有趣的变量之一是 struct uart_ops 指针,它定义了串行核心用于回调到特定于端口的串行驱动程序的函数列表。此结构定义为:

struct uart_ops {
  unsigned int    (*tx_empty)(struct uart_port *);
  void   (*set_mctrl)(struct uart_port *,
                      unsigned int mctrl);
  unsigned int    (*get_mctrl)(struct uart_port *);
  void   (*stop_tx)(struct uart_port *,
                    unsigned int tty_stop);
  void   (*start_tx)(struct uart_port *,
                     unsigned int tty_start);
  void   (*send_xchar)(struct uart_port *, char ch);
  void   (*stop_rx)(struct uart_port *);
  void   (*enable_ms)(struct uart_port *);
  void   (*break_ctl)(struct uart_port *, int ctl);
  int    (*startup)(struct uart_port *);
  void   (*shutdown)(struct uart_port *);
  void   (*change_speed)(struct uart_port *,
                         unsigned int cflag,
                         unsigned int iflag,
                         unsigned int quot);
  void        (*pm)(struct uart_port *,
                    unsigned int state,
                unsigned int oldstate);
  int        (*set_wake)(struct uart_port *,
                         unsigned int state);
  /*
   * Return a string describing the port type
   */
  const char *(*type)(struct uart_port *);
  /*
   * Release IO and memory resources used by
   * the port. This includes iounmap if necessary.
   */
  void   (*release_port)(struct uart_port *);
  /*
   * Request IO and memory resources used by the
   * port.  This includes iomapping the port if
   * necessary.
   */
  int    (*request_port)(struct uart_port *);
  void   (*config_port)(struct uart_port *, int);
  int    (*verify_port)(struct uart_port *,
                        struct serial_struct *);
  int    (*ioctl)(struct uart_port *, unsigned int,
                  unsigned long);
};

这是一个非常大的结构,包含许多不同的函数指针,看起来几乎和 tty_driver 结构一样糟糕。

startup 函数在每次发生 open(2) 调用时调用一次。它仅在串行核心完成大量资源检查并确定需要打开端口后才调用。串行驱动程序通常会执行一些特定于硬件的设置,以允许在此函数中使用端口。

shutdown 函数与 startup 函数相反。当端口关闭且所有数据都已停止流经它时,将调用它。这是告诉硬件停止的位置,并且应释放 startup 函数中分配的任何资源。

request_port 和 release_port 函数用于保留与串行端口相关的内存和其他硬件资源。config_port 函数很像 request_port,但它在硬件可以自动探测连接到它的任何串行端口时调用,并且还负责执行与 request_port 相同的硬件保留。

change_speed 函数在每次需要修改端口线路设置时调用。传递给此函数的值已经从通过 tty 层传递的原始 termios 结构中清除,这使得串行驱动程序中的逻辑更加简单。

有许多函数用于获取和设置串行线路状态和端口状态,这些函数是:

  • set_mctrl:为 MCR UART 寄存器设置新值。

  • get_mctrl:获取当前 MCR UART 寄存器值。

  • stop_tx:停止端口发送数据。

  • start_tx:启动端口发送数据。

  • tx_empty:返回端口发送器是否为空。

  • send_xchar:告诉端口向主机发送 XOFF 字符。

  • stop_rx:停止接收数据。

  • break_ctl:通过端口发送 BREAK 值。

  • enable_ms:启用调制解调器状态中断。

有两个与串行端口的电源管理问题相关的函数:pm 和 set_wake。如果您的硬件平台支持电源管理,请使用这些函数来处理硬件的启动和关闭电源。

verify_port 被调用以验证传递给它的设置对于特定的串行端口是否是有效设置,并且在用户在端口上进行 TIOCSSERIAL ioctl(2) 调用时调用(有关 TIOCSSERIAL 的更多信息,请参阅“tty 层,第二部分”,LJ 2002 年 10 月刊)。

串行驱动程序层处理许多常见的串行 ioctl,例如 TIOCMGET、TIOCMBIS、TIOCMBIC、TIOCMSET、TIOCGSERIAL、TIOCSSERIAL、TIOCSERCONFIG、TIOCSERGETLSR、TIOCMIWAIT、TIOCGICOUNT、TIOCSERGWILD 和 TIOCSERSWILD。如果在串行端口上调用任何其他 ioctl,它将通过 uart_ops 结构中的 ioctl 回调传递到特定端口。

最后一个剩余的函数是 type,它用于返回描述串行端口类型的字符串。这用于位于 /proc/tty/driver/ 中的 proc 文件以及发现端口时的初始启动消息中。

数据在哪里?

您可能已经注意到 struct uart_ops 中没有发送或接收数据的函数。用户通过调用 write(2) 发送到 tty 层的数据由串行驱动程序层放置在循环缓冲区中,并且由特定的 UART 驱动程序负责拾取此数据并将其从端口发送出去。通常,每次发生 UART 中断时,驱动程序都会检查循环缓冲区,以查看是否还有要发送的数据。以下示例函数展示了一种实现此目的的方法:

static void tiny_tx_chars(struct uart_port *port)
{
    struct circ_buf *xmit = &port->info->xmit;
    int count;
    if (port->x_char) {
        /* send port->x_char out the port here */
        UART_SEND_DATA(port->x_char);
        port->icount.tx++;
        port->x_char = 0;
        return;
    }
    if (uart_circ_empty(xmit) ||
       uart_tx_stopped(port)) {
        tiny_stop_tx(port, 0);
        return;
    }
    count = port->fifosize >> 1;
    do {
        /* send xmit->buf[xmit->tail]
         * out the port here */
        UART_SEND_DATA(xmit->buf[xmit->tail]);
        xmit->tail = (xmit->tail + 1) &
                     (UART_XMIT_SIZE - 1);
        port->icount.tx++;
        if (uart_circ_empty(xmit))
            break;
    } while (--count > 0);
    if (uart_circ_chars_pending(xmit) <
       WAKEUP_CHARS)
        uart_event(port, EVT_WRITE_WAKEUP);
    if (uart_circ_empty(xmit))
        tiny_stop_tx(port, 0);
}

该函数首先查看是否指定了 x_char 在此时发送出去。如果是,则发送出去并增加从端口发送出去的字符数。如果不是,则检查循环缓冲区以查看其中是否有任何数据,以及端口当前是否未被任何东西停止。如果为真,我们开始从循环缓冲区中取出字符并将它们发送到 UART。在此示例中,我们最多发送 FIFO 大小除以 2 的大小,这是一个很好的平均发送字符数。发送字符后,我们查看是否已从循环缓冲区中刷新了足够的字符,以请求发送更多字符给我们。如果是,我们使用 EVT_WRITE_WAKEUP 参数调用 uart_event(),告知串行核心通知用户空间我们可以接受更多数据。

串行驱动程序接收到的任何数据都像普通的 tty 驱动程序一样传递到 tty 层,通过调用 tty_insert_flip_char()。这通常在 UART 中断函数中完成。

小型串行示例

列表 1 [可在 LJ FTP 站点 ftp://ftp.linuxjournal.com/pub/lj/listings/issue104/6331l1.txt 获取] 显示了如何向串行驱动程序层注册串行驱动程序以及如何注册单个串行端口的示例。此串行端口与之前的 tty 示例串行驱动程序非常相似,因为它表现得好像每两秒接收到一个字符,只要端口是打开的。它还会将发送给它的任何字符通过调用 write(2) 打印到内核调试日志。

结论

在本文中,解释了新串行驱动程序层的接口,详细介绍了如何注册串行驱动程序,然后注册单个串行端口。由于 2.5 内核中引入了新的驱动程序层,串行驱动程序可以小得多,创建新驱动程序也变得更容易,从而使程序员远离 tty 层的复杂性。

Greg Kroah-Hartman 目前是 Linux USB 和 PCI 热插拔内核维护者。他在 IBM 工作,从事各种与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。

加载 Disqus 评论