十一、UART&TTY驱动

Linux系统中UART驱动和TTY驱动两者有着紧密的关系,它们不像I2C和SPI驱动是单独一个模块,分析时应当将它们看成一个整体来分析。UART驱动部分依赖于硬件平台,而TTY驱动和具体的平台无关。本文的分析内容基于IMX6DL硬件平台和Kernel 3.0.35版本,虽然UART部分依赖于平台,但是不管是哪个硬件平台,驱动的思路都是一致的,下面分模块来分别介绍。

一、UART驱动

UART驱动主要涉及的驱动文件是imx.c、serial_core.c两个文件。首先我们找到驱动的入口函数module_init(imx_serial_init),在函数imx_serial_init中调用uart_register_driver向内核注册了一个驱动,在该函数中除了做常规的初始化驱动之外,有两个关键点的函数调用需要我们注意一下,如下图:

先是调用tty_set_operations将uart_ops这一个tty设备的操作函数集设置到了tty驱动中,同时调用tty_register_driver函数向内核注册了tty驱动,其中uart_ops的数据类型及内容如下:

当调用tty_open函数时就会调用这里的uart_open,具体是怎么调用的,我们后面会分析到。imx_serial_init函数中还调用platform_driver_register向内核注册了一个平台设备,所以UART驱动即是平台设备又是字符设备。当驱动和设备匹配时会调用serial_imx_probe函数,在该函数中除了做具体平台相关的串口端口设置,比如调用platform_get_resource获取中断资源,赋值sport->timer.functioni = mx_timeout设置定时器之外,还有一个关键的操作就是sport->port.ops = &imx_pops,赋值了跟具体硬件平台的底层操作函数,当中的imx_pops结构体如下:

该结构体中的函数都是和具体的硬件平台相关,串口的数据接收、注册中断接收函数、使用DMA接收数据等操作都是在上面的函数中完成,这些函数由NXP官方提供,是和底层硬件最接近的函数。

跟其他的驱动一样,当打开串口设备时,uart_open函数得到调用,在tty_open函数中调用了uart_startup函数来启动串口,如下:

在uart_startup函数中通过uport->ops->startup(uport);间接调用到了imx_startup函数,因为我们在前面已经通过sport->port.ops = &imx_pops将相关硬件平台的串口操作函数赋值给了抽象的串口端口操作函数,所以到这里我们转去分析imx_startup看看里面做了什么操作。

在imx_startup中通过调用request_irq(sport->rxirq, imx_rxint, 0, DRIVER_NAME, sport)注册了串口中断接收函数imx_rxint,串口中断发送函数同理,同时如果板级文件中设置启用了DMA,还初始化了用于DMA数据处理相关的工作队列,如下图:

我们并未配置使用DMA,所以只分析中断接收函数imx_rxint。Imx_rxint函数如下:

imx_rxint函数在循环中读取数据寄存器的值,并在函数的末尾调用了两个很关键的函数,分别是tty_insert_flip_char(tty, rx, flg)和tty_flip_buffer_push(tty),其中tty_insert_flip_char函数的作用是将接收到的字符放入tty数据块中,如下图:

而tty_flip_buffer_push(tty)则是将tty数据块的数据推到线路规程当中,线路规程相关的知识我们后面会讲到,这个函数的作用就类似于通知tty去线路规程获取从串口过来的数据,函数内容如下:

其中有个关键的操作就是调用了工作队列,具体这个工作队列是在何时被注册或者初始化,我们后面讲tty时候会分析到。总结以上,如果中断函数中只调用tty_insert_flip_char函数的话,tty是没办法获取串口数据的,还必须使用tty_flip_buffer_push函数将数据推到线路规程当中去。至此,UART到TTY这条路径我们就分析完了,接下来分析TTY的框架。

一、TTY驱动

TTY驱动不依赖具体的硬件平台,主要涉及的文件是tty_io.c、tty_ldisc.c。TTY驱动框架中包含一个叫线路规程的核心模块,TTY驱动不能直接从UART获取数据,所有的数据都必须从ldisc(线路规程获取)。首先我们来看tty相关的初始化,在前面注册UART驱动的时候,同时调用了tty_register_driver(normal)函数向内核注册了一个tty驱动,在该函数中调用了cdev_init(&driver->cdev, &tty_fops),向设备绑定了tty设备的操作函数集,tty_fops的数据类型是struct file_operations,该变量如下图:

因此当应用层打开一个tty设备时候会调用这个函数集当中的tty_open函数,接下来我们看tty_open函数里面做了什么操作。在tty_open函数中调用tty_init_dev(driver, index, 0)函数对tty设备进行了初始化,在tty_init_dev函数中又调用了initialize_tty_struct(tty, driver, idx)函数对tty相关的结构体进行了初始化,如下图所示:

其中有三个地方需要我们重点关注,第一个是tty_ldisc_init(tty),调用该函数完成了线路规程的初始化,在tty_ldisc_init函数里面通过调用tty_ldisc_get获得线路规程,在tty_ldisc_get函数中通过调用get_ldops(disc)获得线路规程的操作函数,如图所示:

其中tty_ldiscs是一个全局数组,数组元素类型是struct tty_ldisc_ops,也就是线路规程的操作函数集,类型如下图:

线路规程的操作函数具体是在什么时候被赋值初始化的,我们后面会分析到。

在initialize_tty_struct函数中第二个需要我们关注的函数调用是tty_buffer_init(tty),,

调用该函数完成了tty数据块相关的初始化,如下图所示:

在初始化函数中还初始化了一个工作队列,INIT_WORK(&tty->buf.work, flush_to_ldisc)。

具体这个工作队列是在何时被调用呢?就是在我们前面分析imx_rxint中断接收函数时,调用了tty_flip_buffer_push,在该函数中通过schedule_work(&tty->buf.work)调度了该工作队列。至此,TTY也和UART联系上了。

在initialize_tty_struct函数中需要我们关注的地方是tty->ops = driver->ops语句。前面我们分析到,在串口注册时候调用tty_set_operations函数,通过driver->ops = op将tty的操作函数赋值给了uart驱动,在这里则是将注册进去的函数给拿出来赋值给了tty设备,等于是应用层操作tty设备就是操作uart串口。在tty_init_dev函数中,除了初始化tty设备之外,还调用tty_ldisc_setup(tty, tty->link)函数对线路规程进行了设置。在tty_ldisc_setup函数中调用了tty_ldisc_open函数,该函数中使用ld->ops->open(tty)打开了线路规程,但是线路规程的操作函数是在哪里进行赋值的呢?保留这个疑问,我们接下来分析线路规程相关的初始化流程。

记得前面我们提到的一个全局数组tty_ldiscs吗?这个数组的元素类型就是线路规程的操作函数。我们在内核代码中进行全局搜索,发现在tty_register_ldisc函数中进行了设置,如下图:

调用该函数的话,就会将线路规程设置到全局数组tty_ldiscs中,那么tty_register_ldisc函数是在哪里被调用的呢?答案是,在tty_ldisc_begin函数中被调用,如下图:

而tty_ldisc_N_TTY变量就是线路规程的操作函数,变量赋值如下图:

tty_ldisc_begin这个函数被console_init调用,那是谁又调用了console_init呢?答案是在/init/main.c文件中,asmlinkage void __init start_kernel(void)函数调用了console_init。而start_kernel函数正是内核的入口函数。也就是说,在进入内核的时候,第一时间就先初始化了tty的线路规程,赋值了线路规程的相关操作函数。那线路规程的操作函数又是在哪里被调用的呢?

前面我们讲过,tty驱动不能直接从串口获得数据,数据的来源是线路规程,那么调用线路规程的读写函数只能是tty的操作函数,所以我们来看看之前从未分析的tty_read和tty_write函数。首先来看tty_read函数,如下图:

果不其然,在tty_read中通过ld->ops->read调用了线路规程的read函数,也就是调用了tty_ldisc_N_TTY的ntty_read函数。我们再来看tty_write函数,如下图:

同样是调用到了线路规程的n_tty_write函数。

综上,在进入内核的时候,先是设置了线路规程的操作函数,然后在tty驱动注册的时候设置了tty的操作函数,并在后续打开tty设备时调用tty_open函数,在open函数中通过get_ldops(disc)获得线路规程的操作函数。当应用层调用tty_read读取数据时就调用了n_tty_read获得了数据。