王巧玲,高 杰,陈娅荔
基于linux操作系统的多串口驱动研究
*王巧玲1,高 杰2,陈娅荔1
(1.井冈山大学电子与信息工程学院,江西,吉安 343009 2.南昌大学共青学院信息工程系,江西,共青城 332020)
在介绍基于linux操作系统的多串口驱动设计理论基础上, 重点研究对多串口编程的技术,实现基于tty的核心设计,可作为实现串口终端设备集中管理、实时数据采集的服务器主板应用。其特点是可以采用ARM平台,但完全兼容X86平台,用户原来在X86 平台下编写linux操作系统的多串口驱动的程序只需做一次重新编译即可实现移植,达到基于Linux操作系统的多串口驱动实时通讯的目的。
linux操作系统;多串口驱动;tty核心
在Linux操作系统中串口的编程技术和DOS、Windows系统下的方法有所不同。本文较为详细的叙述了在Linux操作系统中串口的驱动编程技术。重点研究多串口驱动通讯,本驱动创新点可以通过自动探测的方法测出当前是什么类型的主板,可以让bios往中断状态寄存器或其他保留的寄存器位中写一个标记即可,用于唯一标记每一类主板。串口驱动主要技术是在tty核心[1]的基础上实现的,需要同时驱动最多32个串口,板上的串口共用中断号,板上串口基地址连续。因此,通过合理的结构体设计可以方便的实现。
串口驱动本质上也是字符设备驱动[2-3],只是内核根据其特殊的数据传输过程进行封装,将其文件操作(file_operation)封装到tty核心(tty_io.c)中,由tty核心提供接口给应用程序调用,tty核心还提供接口给线路规程和驱动程序使用;因此,在写串口驱动时只需要调用tty核心提供的接口,并传入驱动的处理函数(tty_operations)。
向tty核心注册驱动时,tty核心便会为该驱动分配设备号,并注册文件操作函数(file_operations)。而在tty核心中各文件操作函数会调用驱动传入的处理函数,可见真正实现应用程序需求的函数还是驱动程序,tty核心只是实现一个中转的过程,但tty核心中封装了很多串口驱动实现的通用函数,大大简化了串口驱动的编写。
编写串口驱动主要分以下几个步骤:
int tty_register_driver(struct tty_driver *driver);在注册前要对tty_driver进行赋值,在tty_driver中指定设备号、串口个数、操作函数等信息。
int tty_unregister_driver(struct tty_driver *driver);释放设备驱动。
设置驱动提供的串口操作函数,注册时提供给tty核心使用,串口操作函数是在tty_driver中设置的。
中断处理函数是串口通讯的核心,驱动对串口进行读、写都是通过中断来实现的;串口内部有2个64B的FIFO(TX和RX),分别用于发送和接收数据。
图1 串口数据发送
串口发送数据过程如图1所示,一般串口通讯都是基于异步协议的,因此数据是以字节为单位传输的,当驱动将一个字节数据写到串口时,会先通过THR寄存器存入RX FIFO,然后通过TSR将数据发送出去,每一个数据的发送时间是16个时钟周期。当RX FIFO里的数据低于某一TRIGGER值或为空时,就会触发中断,并将LSR寄存器的bit[0]和ISR的bit[1]置1,中断处理程序根据这些值发送数据到串口。RX FIFO的TRIGGER值由FCR寄存器的bit[6,7]设置。
串口接收数据过程如图2所示,和发送数据一样,串口接收数据也是以字节为单位的,串口通过RSR寄存器接收数据,然后会将数据存入TX FIFO,驱动程序同样以字节为单位从TX FIFO中取数据。当TX FIFO中数据高于某个TRIGGER值就会触发中断,并将LSR的bit[5]和ISR的bit[2]置1,中断处理程序根据这些值从串口TX FIFO读取数据。TX FIFO的TRIGGER值由FCR寄存器的bit[4,5]设置。
当启用了自动RTS硬件流控制时(EFR bit[6]=1 MCR bit[1]=1),当TX FIFO的数据大于56时,串口会自动通知发送端停止发送数据;当TX FIFO数据小于8时,串口会自动通知发送端继续发送数据。使用RTS/CTS(请求发送/清除发送)流控制时,应将通讯两端的RTS、CTS线对应相连。
图2 串口数据接收
多串口驱动基于tty核心进行的,主要设计以下几个方面:
这里的与内核通讯实际上是通过tty核心来实现的,通过向tty核心注册驱动告诉内核应该处理哪个驱动和应该使用哪些处理函数,注册函数如下:
int tty_register_driver(struct tty_driver *driver);
字符设备驱动与应用程序通讯是通过文件操作file_operations来实现的,而文件操作是在tty核心中实现的。
可见,应用程序虽然是调用tty核心的文件操作函数,但本质上还是驱动来实现的。因此,驱动中可以根据需要实现与应用程序通讯的函数,本人在多串口驱动中实现了open、release、write、ioctl等函数。
驱动与硬件通讯就是操作寄存器,在串口芯片的datasheet中有详细的说明,多的串口芯片是ST16C654,这里介绍几个驱动中常用的寄存器。
RHR:接收数据寄存器;
THR:发送数据寄存器;
IER:中断使能寄存器,其中bit[0-3]分别对应4中中断;
ISR:中断状态寄存器,其中bit[1,2]分别代表读、写中断;
FCR:FIFO控制寄存器,使能FIFO和TRIGGER等级都在这里设置;
LCR:线路控制寄存器,设置线路规程,包括:数据位,校验位,停止位等;
MCR:MODEM控制寄存器,其中bit[0,1]用于使能硬件流控制输出;
LSR:线路状态寄存器,其中bit[0]标记RHR是否有数据,
bit[5]标记THR是否为空;
DLL、DLM:波特率除数寄存器,用于设置波特率。
这里的数据结构不是指上一节的数据结构,这里是指串口驱动内部的数据组织方式,这些数据结构贯穿整个串口驱动程序,合理的设计数据结构和组织方式可以有效地提高驱动的效率,下面分别介绍内核和多串口驱动的数据结构设计和组织方式,并进行比较和分析。
数据结构和组织方式的设计要注意以下几个方面:
(a)数据结构要能表示串口的所有信息以及运行过程中串口的属性;
(b)open时要能根据数据结构来判断是哪一个串口,因为有可能一块板上有很多串口;
(c)中断处理函数中也需要能通过数据结构来判断是哪个串口产生的中断。
内核串口驱动主要是通过以下2个数组来实现的。
struct serial_state rs_table[64];
这是一个串口基本信息结构体,结构体中包括波特率、基地址、中断号等信息,在定义时就已经对其进行了初始化,初始化是通过宏SERIAL_PORT_DFNS来实现的,这个宏初始化了每个串口的基地址、中断号,这个数组中的初始值就是默认情况下的ttySn的值,所以可以根据主板的串口信息修改该宏的值,实现驱动主板上各串口。
struct async_struct *IRQ_ports[225];
这是一个数组指针,数组大小为225,对应于每个中断号,数组每个元素指向所有共享该中断号的串口信息结构体链表;async_struct是一个包含一个已打开串口所有信息的结构体,当打开一个串口时,会动态创建一个串口信息结构体async_struct,然后根据打开设备的基本信息serial_state对这个结构体进行初始化,最后将其链入相应IRQ链表的头。
低版本内核串口驱动的数据结构模型如图3所示,在打开设备时,通过打开设备的次设备号和驱动的起始次设备号计算出是第几个串口,从而得到串口基本信息serial_state;在中断处理程序中,可以直接根据中断号获取所有使用该中断号的串口信息,然后对所有串口进行处理。
图3 低版本内核数据结构模型
高版本内核串口驱动的设计思想和低版本内核类似,只是在其基础上进行了一些改进,以8250串口驱动为例说明,主要有以下几个数据结构:
struct old_serial_port old_serial_port[64];
串口基本信息结构体,类似于2.4内核的serial_state结构体,由宏SERIAL_PORT_DFNS初始化。
struct uart_8250_port serial8250_ports[64];
已打开串口信息结构体,类似于低版本内核的async_struct结构体,与低版本内核的不同之处在于这里的结构体是静态分配的。
struct irq_info irq_lists[225];
这是一个数组,类似于低版本内核的IRQ_ports,数组的内容为包含一个list_head类型变量的结构体,用于指向相同中断号的所有串口的链表头。
内核串口驱动的数据结构设计是一种通用的设计,目的在于能应用于各种不同的主板,而多串口驱动的数据结构完全是针对104串口系列主板设计的,是根据多的特性设计结构体。
由于每块多主板的所有串口是有一些共用属性的,所以设计一个主板信息结构体,这个结构体每块主板一个,主板各个串口信息都可以通过此结构体来设置,结构体定义如下:
struct evocdrv_brdconf {/*每块板的信息*/
int board_type;/*主板类型1-1048COM 2-COMMON*/
int flags;/*UART_CLEAR_FIFO | UART_USE_ FIFO |UART_HAS_EFR*
int ports;/*串口个数*/
int irq;/*中断号,一般为共享中断*/
int irq_type;/*0:串口0-7共享中断(M1=on);1--0-3共享,4-7共享(M1=off)*/
int vector;/*中断状态寄存器基地址,4块板16个端口,板上所有串口共用*/
int vector_mask;/*中断状态*/
int uart_type;/*芯片类型*/
char uart_name[20];/*芯片名字*/
int ioaddr;/*每个串口的端口基地址,由sw1设置(p14)*/
int baud_base;/*串口的波特率基数quot=baud_ base/baud*/
int max_baud;/*最大波特率*/};
除了主板信息结构体之外,还定义了一个大小为32的串口信息结构体数组,用于保存每个串口的信息,串口信息数组在初始化时根据其主板的信息设置;由于多主板的特殊性,如:主板上的所有串口是共用中断号的,串口的基地址是连续的等等,所以可以把这些共用的属性设置在主板信息结构体中。串口信息数组是这样设计的,默认每块主板8个串口,若主板串口个数小于8则数组的相应位为空值。
打开串口时,根据设备的次设备号和驱动的次设备号起始值,计算出打开的是第几个串口,根据这个就可以直接从串口信息数组中获取串口信息;在中断处理程序中,这里并没有像内核串口驱动中的中断指针数组,但由于每块板上的串口中断号是相同的,所以只需要在发生中断时判断是哪块板上的串口产生的,然后对该板上的串口进行一一判断并处理;在请求中断号时,将板上第一个串口信息结构体作为参数传入,然后在中断处理函数中根据串口信息数组的第0,8,16,24个元素来判断属于哪块主板。
串口驱动中要实现的函数有很多,包括与tty核心的通讯函数,与应用程序的通讯函数以及中断处理函数,主要函数如下:
struct tty_operations evocdrv_ops = {
.open = evocdrv_open,/*打开串口*/
.close = evocdrv_close,/*关闭串口*/
.write = evocdrv_write,/*写串口*/
.put_char = evocdrv_put_char,/*写一个字符到串口*/
.flush_chars = evocdrv_flush_chars,/*发送写缓冲区中数据*/
.write_room = evocdrv_write_room,/*获取写缓冲区的可用空间*/
.chars_in_buffer = evocdrv_chars_in_buffer,/*获取写缓冲区数据大小*/
.flush_buffer = evocdrv_flush_buffer,/*清空写缓冲区*/
.ioctl = evocdrv_ioctl,/*ioctl*/
.throttle = evocdrv_throttle,/*通知串口不要发送数据*/
.unthrottle = evocdrv_unthrottle,/*通知串口恢复发送数据*/
.set_termios = evocdrv_set_termios,/*设置线路规程*/
.stop = evocdrv_stop,/*停止向串口发送数据*/
.start = evocdrv_start,/*恢复向串口发送数据*/
.hangup = evocdrv_hangup,/*设备挂起,等待再次被open*/
.tiocmget = evocdrv_tiocmget,/*获取MODEM状态位*/
.tiocmset = evocdrv_tiocmset,/*设置MODEM状态位*/
};
这里挑一些与应用程序联系比较密切的函数进行介绍。
(1) open
当应用程序调用open函数打开设备文件时就会进入此函数,此函数主要实现打开设备以及对设备进行初始化等功能,主要工作如下:
● 找到打开串口的信息结构体并进行必要的初始化;
● 初始化串口寄存器,包括启用FIFO、中断等各种初始化工作;
● 设置波特率、停止位、流控制等线路状态信息;
(2) close
当应用程序调用close函数关闭设备时会进入此函数,此函数实现的功能与open相反,主要是善后工作,包括对所有改变过的寄存器复位,还有对open后请求的一些资源进行释放。
(3) write
当应用程序调用write函数进行写操作时会进入此函数,此函数主要实现把应用程序写入的数据保存到写缓冲区xmit_buf中,这是一个4KB大小的缓冲区,是在open时动态分配的,在close是释放。当xmit_buf中有数据时,开启发送数据中断IER_THRI,然后就会进入中断处理程序将xmit_buf的数据发送给串口。
(4) ioctl
当应用程序调用ioctl函数时会进入此函数,此函数主要提供应用程序一些设置的方法和获取驱动信息的方法,这里有一个很重要的方法TIOCSSERIAL,当调用setserial命令设置设备节点信息时会调用ioctl并传入TIOCSSERIAL参数,在处理程序中,根据用户传入的基础波特率、中断号、基地址等串口信息修改串口信息结构体,如果中断号或基地址改变了,则重新设置串口各寄存器,否则只要重新设置串口的线路信息就可以了。
(5) set_termios
当应用程序调用tcsetattr函数时进入此函数,主要功能是设置线路规程信息,函数tcsetattr调用后tty核心会更新tty_struct->termios,并将原来的线路规程保存在old_termios中作为参数传入set_termios中,程序根据tty_struct中的线路规程值对串口进行重新设置。
(6) tiocmset
设置MODEM状态,主要是设置DTR/RTS线的状态,主要应用于硬件流控制中。
(7) 中断处理函数
串口驱动程序的数据传输和接收都是通过中断来实现的,触发中断的情况有很多,这里列出几个常用的中断触发。当出现以下几种情况时会触发中断:
①若设置了IER寄存器的RDI位,当FIFO值到达TRIGGER值时触发读中断,并将ISR寄存器的bit[1]和LSR寄存器的bit[0]置1;
②若设置了IER寄存器的THRI位,当FIFO值低于TRIGGER值或FIFO为空时触发写中断,并将ISR寄存器的bit[0]和LSR寄存器的bit[5]置1;
③若设置了IER寄存器的TLSI位,当出现RX break、RX framing error、RX parity error、overrun error的任意一个时将触发一个中断;
④若设置了IER寄存器的MSI位,当CTS线、DSR线、RI响铃控制器、CD数据载波检测线中任意一个状态改变时将触发一个中断;
由于有很多中情况会触发中断,所以在中断处理函数中要对各种可能产生中断的情况进行判断并处理,这里最重要的当然是对读、写中断的处理了,可以通过读取寄存器的值来判断进行哪种操作。
当ISR寄存器的bit[1]或LSR寄存器的bit[0]为1时,执行读操作,即从串口读取数据,由于串口内部有个64B的RX FIFO,而且串口数据传输是以字节为单位的,所以在读取数据操作函数(receive_chars)中,每次以字节为单位从串口RHR寄存器读取64B的数据;先将读取的数据保存到tty_buffer中(tty_insert_flip_char),然后在将tty_ buffer中的数据发送到read_buf中(tty_flip_buffer _push),应用程序就是从read_buf中读取数据的。
当ISR寄存器的bit[0]或LSR寄存器的bit[5]为1时,执行写操作,即向串口写数据(transmit_ chars),写操作与读操作类似,也是每次以字节为单位向串口写入64B的数据,写入数据的寄存器为THR;这里写入的数据并不是用户空间传入的数据,应用程序先将数据写入写缓冲区xmit_buf,函数transmit_chars从写缓冲区发送数据到串口,写缓冲区大小为4KB。
可以看出内核中数据结构的设计是一种通用的设计,不论是哪种类型的主板、哪种类型的芯片都可使用内核驱动;但正是由于这种通用性,使得内核对串口信息的默认值很难做一个合适的设置,因为不同的主板串口信息往往是不同的,只能通过serserial命令来手动修改。多串口驱动的设计是将内核中2个串口信息结构体合为一个,并实现一个主板信息结构体,此结构体包含了一块板上所有串口共用的信息,对于中断处理的设计是通过传入每块板第一个串口的信息结构体实现的,这里没有内核方便,有待改进。
[1] 李萌.linux操作系统多串口编程技术[J].火控雷达技术; 2008,29(3).
[2] 刘海燕,荆涛.linux系统应用与开发教程[M].北京:机械工业出版社,2006:44-47.
[3] 高艳辉,龚华军.嵌入式实时Linux在PC104平台上的实现[J].仪器仪表用户,2008,15(2).
[4] 高艳辉.基于嵌入式实时Linux的无人机半物理仿真平台研究[D].南京:南京航空航天大学,2008.
[5] 曾炜,沈为群.基于RTAI-Linux的飞行仿真实时管理系统[J].计算机工程,2008,34(19).
[6] 刘海燕,邵立嵩.linux系统应用与开发教程[M].北京:机械工业出版社,2006:65-69.
[7] 黄志洪,钟耿扬.linux操作系统[M].北京:冶金工业出版社,2010:155-169.
The research of multi-serial drive based on the Linux operating system
*WANG Qiao-ling1,GAO Jie2, CHENG Ya-li1
(1.School of Electronic Information Engineering, Jinggangshan University, Ji’an, Jiangxi 343009, China;2.Information Engineering Department of Nanchang University, Gongqing College, Gongqing City, Jiangxi 332020,China)
We introduce the Linux operating system based on the design theory of multi-serial port drive and realize the core design based on tty with the multi-serial port programming technology, which can be used as a serial port terminal equipment to realize the centralized management, the real-time data acquisition server application motherboard. Its characteristic is can be used with ARM platform, but is fully compatible with X86 platform. Furthermore, the original X86 operating system Linux multi-serial port drive program in the workbench need only recompile to realize the transplant, which is based on Linux operating system to the serial driver real-time communication.
Linux operating system; multi- serial driver; tty core
1674-8085(2012)03-0069-06
TP316
A
10.3969/j.issn.1674-8085.2012.03.015
2012-02-16;
2012-03-27
*王巧玲(1979-),女,江西吉水人,讲师,硕士,主要从事计算机软件理论研究(Email:xinyuwql@163.com);
高 杰(1983-),男,江西吉安人,助教,主要从事嵌入式方向研究(Email:275750383@qq.com);
陈娅荔(1982-),女,江西南昌人,助教,硕士,主要从事人工智能和图像处理研究(Email:30006959@qq.com).