在Linux系统中,终端是一类字符型设备,它包括多种类型,通常使用tty来简称各种类型的终端设备。
1 串口终端(/dev/ttyS*)
串口终端是使用计算机串口连接的终端设备。Linux把每个串行端口都看作是一个字符设备。这些串行端口所对应的设备名称是/dev/ttySAC0; /dev/ttySAC1……
2 控制台终端(/dev/console)
在Linux系统中,计算机的输出设备通常被称为控制台终端(Console),这里特指printk信息输出到的设备。/dev/console是一个虚拟的设备,它需要映射到真正的tty上,比如通过内核启动参数” console=ttySAC0”就把console映射到了串口0。
3 虚拟终端(/dev/tty*)
当用户登录时,使用的是虚拟终端。使用Ctcl+Alt+[F1—F6]组合键时,我们就可以切换到tty1、tty2、tty3等上面去。tty1–tty6等称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名.
Linux tty子系统包含:tty核心,tty线路规程和tty驱动。tty核心是对整个tty设备的抽象,对用户提供统一的接口,tty线路规程是对传输数据的格式化,tty驱动则是面向tty设备的硬件驱动。
分析串口驱动程序要先记住两个文件sumsung.c和s3c6400,这两个文件很重要,路径是在…/drivers/tty/serial 下。
首先,当用户使用write函数通过串口发送数据的时候,write通过系统调用会找到file_operation中相应的指针,tty_fops这个结构就是对应的file_operations,其中tty_write来进行响应系统调用。
接下来进入到tty_write函数中,在tty_write函数中找到了线路规程ops,我们这里需要找到线路规程,tty_ldisc_N_TTY结构就是我们需要找的一个结构,在这个结构中,可以看到n_tty_write,这个就是tty_write需要调用的函数。
接下来我们再进入到n_tty_write函数中看一下,可以看到n_tty_write会调用ops,ops实际上是一个tty_operations,这里我们需要知道由tty_operations定义的一个结构uart_ops,这个结构就是我们想要的ops,其中又有uart_write这个函数。
至此,在tty层面上就已经把write的调用关系给列出来了,简单的说,就是用户在执行write系统调用的时候,首先是找到tty_write,然后再找到n_tty_write,最后找到了uart_write函数。接下来我们就要看看在驱动中怎么进行系统调用并主要学习一些重要的数据结构。
在uart_write函数中首先会使用到uart_state数据结构,还有一个是uart_port,我们可以看出uart_port是从state中获取到的。
在port中,我们还可以得到一个数据结构uart_ops,在这个数据结构中,有很多函数指针,利用这些函数指针可以操作硬件。
除了这三个数据结构外,还有一个重要的数据结构uart_driver,我们可以回去看看uart_write这个函数,第一行tty->driver_data,说明了state是根据driver_data中获得的,那么driver_data又是从哪里获得的呢,我们可以打开uart_open函数一探究竟,这个函数中又得到这样的信息,driver_data是从state获得的,而state是通过uart_get这个函数通过driver获得的。这样我们就把uart_driver,uart_state ,uart_port ,uart_ops这四个比较重要的数据结构关系给大致理清楚了。
下面列出了几个重要的数据结构,接下来我们就深入来理解一下这些结构,并开始分析初始化。
• UART驱动程序结构:struct uart_driver
一个uart_driver代表一个串口驱动
• UART端口结构: struct uart_port
一个uart_port对应一个串口 (一个驱动可以包含多个串口)
• UART相关操作函数结构: struct uart_ops
uart_ops里全部是函数指针,对串口硬件实现相关操作
以上三个构成了串口驱动的主体
• UART状态结构: struct uart_state
• UART信息结构: struct uart_info
1 打开Samsung.c,在初始化过程中,使用了uart_register_driver这个函数去注册了串口驱动,后面的s3c24xx_uart_drv就是一个uart_driver,代表一个串口驱动,所以第一项工作就是注册串口驱动。
在s3c6400.c中,又可以看到使用了s3c24xx_serial_init函数,这个函数继续跟进会看到使用了platform_driver_register函数,这就联想到我们上节博客中平台总线驱动的注册了。
2 在平台总线那章我们知道,当驱动和设备进行比较,一旦发现有匹配的时候,就会调用probe函数,这里也一样,我们重点去分析一下这里的probe函数。首先第一个工作是取出相应的uart_port结构—&s3c24xx_serial_ports[probe_index],然后初始化uart_port—s3c24xx_serial_init_port。
继续分析初始化的工作是如何进行的,在s3c24xx_serial_init_port函数中,有几个需要我们掌握的几句代码,首先是platform_get_resource函数获得设备的基地址,然后用静态分配虚拟地址S3C_VA_UART,最后使用platform_get_irq函数取出相应的中断号。
这里还需要注意一个函数s3c24xx_serial_resetport,这个函数主要是reset fifos的功能,进入到这个函数,重点来看复位fifo的寄存器UFCON。在这个寄存器中,通过查找芯片手册可以看到,主要设置第一位和第二位同时为1来设置FIFO的复位,同样在内核中也可以发现,UFCON这个寄存器的第一和第二位被设置为1。
下面我们再返回到probe函数中,还有几个函数需要了解一下:
uart_add_one_port 用来添加端口。
platform_set_drvdata 用来把port放到dev中的driver_data中去。
device_create_file 用来创建一个属性文件,通过这个文件可以看到相关串口信息。
s3c24xx_serial_cpufreq_register 这个函数和动态频率调节有关系。
这里我们先了解一下初始化函数中相关的步骤就可以了。可能第一次接触这么复杂的调用关系确实很难理解,没关系,不然嵌入式那么简单了谁都可以随随便便学会了就没啥含金量了。下面给出这一小节分析的思维导图。
根据上一小节的分析,我们可以大致了解到驱动程序初始化的工作步骤,这个小节我们主要分析一下串口驱动程序是如何打开设备的。
首先打开Samsung.c 进入串口驱动注册程序中初始化中,有个函数uart_register_driver。
进入到uart_register_driver,可以看到调用了一个函数tty_register_driver。
进入到tty_register_driver函数,我们可以看到串口注册函数中使用的是cdev_init,这样就说明了串口也是字符设备,tty_fops实际上就是串口设备文件的file_operations。
继续打开tty_fops,会发现其中有各种设备方法,open,read,write等等,这样我们就找到了open系统调用的响应入口。
tty_open接下来该怎么做呢,这里我们只要知道tty_open 调用到了uart_ops里面的uart_open 函数。
再接下来我们看看uart-open这个函数,打开这个函数,发现这个函数继续调用了uart_startup函数。
我们再继续打开uart_startup函数,可以发现下面这行代码,现在调用到了这个串口驱动函数集的startup,uport是uart_port类型的,代表一个串口,里面含有函数操作集。(操作函数集是什么?别着急,继续看。)那么在哪里找到串口驱动函数集的startup呢,我们就尝试在驱动程序里面找找看。
在驱动程序Samsung.c中,获取port是通过probe函数中s3c24xx_serial_ports这个数组来获取到的。
打开s3c24xx_serial_ports,可以看到每一个port代表一个串口,在port中,有个结构s3c24xx_serial_ops,这个结构就是操作函数集。
在s3c24xx_serial_ops这个结构中,我们就可以找到了startup函数。
总结一下上面的容
1 当用户使用open系统调用时,会找到一个file_operations,tty_fops就充当这个file_operations,然后在tty_fops里就会找到tty_open 函数。
2 在uart_ops中有一个函数uart_open ,这个函数是要被tty_open函数调用的。在uart_open函数中,调用了uart_startup函数。
3 通过驱动程序中s3c24xx_serial_ports这个数组来获得操作函数集s3c24xx_serial_ops,从而找到了startup函数。
接下来我们就来分析一下startup做了什么工作。
这里主要看下面的四行代码
rx_enabled(port)=1; /*使能串口接收功能*/
ret = request_irq(ourport->rx_irq, s3c24xx_serial_rx_chars, 0, s3c24xx_serial_portname(port), ourport); /*为数据接收注册中断处理程序*/
tx_enabled(port) = 1; /*使能串口发送功能*/
ret = request_irq(ourport->tx_irq, s3c24xx_serial_tx_chars, 0, s3c24xx_serial_portname(port), ourport); /*为数据发送注册中断处理程序*/
这一小节我们的分析按照两步来进行,第一步是分析tty数据发送调用关系,第二步是串口发送函数分析。下面先来看第一步。
(1) 数据要发送出去,首先是用户先进行写write函数,内核要响应用户程序的write,肯定是有一个file_operations, 在上小节中我们已经知道了在驱动程序中是如何找到这个file_operations—–tty_fops。在这个结构中就可以找到tty_write函数,这个函数就作为响应用户程序的write函数。忘了可以在第一小节中回忆一下。
(2) tty_write的实现主要依赖于线路规程的n_tty_write函数,(n_tty_write来源于tty_ldisc_N-TTY结构)。忘了可以在第一小节中回忆一下。
(3) n_tty_write会找到tty的file_operations,实际上tty的file_operations就是uart_ops,uart_ops中又有uart_write函数。
uart_write会调用uart_start函数,然后uart_start又找到了_uart_start函数。
_uart_start又会调用到start_tx,start_tx可以看出来源与uart_port中的ops,我们进入到Samsung.c文件中,可以直接找到uart_port中的ops,也就是s3c24xx_serial_ops。
(4) 进入到s3c24xx_serial_ops,就可以找到我们需要的start_tx函数。
上面就是用户的write函数就找到了驱动程序中s3c24xx_serial_start_tx函数的一个过程,也可以说是对第一小节的分析的一个小总结,接下来我们就看看s3c24xx_serial_start_tx这个函数是如何实现的。
我们首先看看s3c24xx_serial_start_tx这个函数是怎么操作的。在这个简短的函数中,并没有发现有操作寄存器的代码去发送数据,原因在于enable_irq这个函数来完成发送数据的工作,而不是start_tx来完成。start_tx来激活中断,中断处理程序来发送数据。
中断处理程序是如何拿到数据进行发送呢,这里我们需要知道循环缓冲这个概念,这个知识很重要。循环缓冲是存放用户执行write系统调用寻找驱动程序中s3c24xx_serial_start_tx函数过程中存放write系统调用本身所带数据的一个区域,简单的说就是存放write中数据的。当串口发送数据的时候,就会取走循环缓冲中存放的write的数据。具体是在writex寻找到start_tx函数过程中uart_write函数中把数据发送到循环缓冲中的,在这个函数中,循环缓冲是circ这个结构,保存在xmit这个成员中。在箭头指向的代码处,表明的是把buf中的数据放到循环缓冲中去的。
我们在这里主要分析串口发送中断处理程序s3c24xx_serial_tx_chars。
在这个函数中,主要分析的是下面三个箭头所指向的部位。
这里按箭头的从上到下的顺序来分析。
1 如果有数据要发送时,就会把数据放到UTXH这个寄存器中,然后就要把状态清零,表示此时没有数据量。
2 当循环缓冲为空或者不允许发送数据的时候就停止发送,因为之前的分析发送数据是打开中断enable_irq就可以了,那么停止发送数据可以想到的是关闭中断。
3 利用while循环来发送数据,循环的条件是循环缓冲不为空和发送的数据量不到256,count大于0,count最多为256。一次中断最多发送256个字符。在这个while循环中:
·寄存器UFSTAT用来接收和发送FIFO,当发送FIFO满的时候,要退出发送。
·当FIFO没有满时,数据从循环缓冲中来,有效数据从循环缓冲的尾部来取(tail),然后写入到UTXH中。
·调整循环缓冲的位置。
发送完数据后 ,还有两件事情,第一个是计算循环缓冲的数据量,当这个数据量低于256时,要做一个uart_write_wakeup,因为此时没有足够的数据量要发送时,就要通知应用程序唤醒应用程序中企图发送数据的进程。
最后一个是当循环缓冲为空的时候,就应该关闭串口发送功能。
总结:以上就是串口发送功能函数s3c24xx_serial_tx_chars的分析,这样一来就大致了解到当用户程序执行write函数后,是怎么样找到驱动程序的start_tx,再接下来是怎么发送用户程序write中本身buf中所带的数据给串口。这里给出思维导图。
(1) 这里按照tty数据发送的方式继续分析,当用户进行read系统调用进行数据接收时,内核要响应用户程序的read,就去找对应的file_operations—–tty_fops。在这个结构中就可以找到tty_read函数,这个函数就作为响应用户程序的read函数。
(2) tty_read的实现主要依赖于线路规程的n_tty_read函数,(n_tty_read来源于tty_ldisc_N-TTY结构)。这里先来看看n_tty_read函数。下面两张图都是n_tty_read函数中的。
这个函数主要做以下几个工作
1 设置阻塞状态TASK_INTERRUPTIBLE
2 判断有没有数据去读,如果没有数据,则会做 schedule_timeout,调度让阻塞生效。
3 如果有数据读,则会从read_buf中读取(copy_from_read_buf),这个read_buf是串口驱动专门存放数据的地方,APP从read_buf中读数据
之前学过发送数据s3c24xx_serial_tx_chars函数,现在接收数据可想而知是s3c24xx_serial_rx_chars函数,这里我们来分析一下s3c24xx_serial_rx_chars函数是怎么工作的。下面两张图完整的给出了s3c24xx_serial_rx_chars函数内容。
可以总结上面函数工作的步骤分为下面几步:
· 读取UFCON寄存器
· 读取UFSTAT寄存器
· 在s3c24xx_serial_rx_fifocnt函数中,fifosize是接收fifo的数据量,如果接收的数据量为0,则退出中断。
· 读取UERSTAT寄存器
· 取出URXH寄存器中接收到的字符
· 做流控的处理(UPF_CONS_FLOW)
· 根据UERSTAT寄存器的值记录具体的错误类型
· 如果收到的是sysrq,则会进行特殊的处理(uart_handle_sysrq_char)
· 把接收的字符送进串口驱动(uart_insert_char)
· 把串口驱动里的数据送到read_buf中去(tty_flip_buffer_push)
总结
在这一节中,我们就把串口程序初始化,以及打开设备,读写操作过程都详细的讲完了,主要是分析系统调用是如何找到驱动程序中的响应函数,比如write是如何找到start_tx函数的,还有在驱动程序中是如何执行start_tx函数的。分析的过程比较痛苦,一个一个嵌套来嵌套去的,坚持坚持驱动的学习还很长。在下节中,我们来尝试编写一个串口驱动程序。