11、UART&TTY驱动

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

1、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的数据类型及内容以下:3d

 

 

当调用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结构体以下:orm

 

 

 

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

跟其余的驱动同样,当打开串口设备时,uart_open函数获得调用,在tty_open函数中调用了uart_startup函数来启动串口,以下:队列

 

 

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

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

 

 

咱们并未配置使用DMA,因此只分析中断接收函数imx_rxint。Imx_rxint函数以下:get

 

 

 

 

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的框架。

1、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得到了数据。

相关文章
相关标签/搜索