

十一、UART&TTY驱动
source link: http://www.cnblogs.com/timemachine213/p/14317462.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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获得了数据。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK