【摘要】本文详细解读了linux系统下的tty子系统的深层次原理和架构,并参考了LDD3中的代码实例讲述了无硬件下实现一个简单的tty设备驱动模块的编写。对了解tty子系统及下部串口驱动模块的学习有较好的参考价值。
tty一词源于Teletypes,或Teletypewriters,它是最早出现的一种终端设备,类似电传打字机,由Teletype公司生产。最初tty是指连接到Unix系统上的物理或者虚拟终端。终端是一种字符型设备,通常使用tty来统称各种类型的终端设备。随着时间的推移,当通过串行口能够建立起终端连接后,这个名字也用来指任何的串口设备。
它还有多种类,例如串口(ttySn、ttySACn、ttyOn)、USB到串口的转换器(ttyUSBn),还有需要特殊处理才能正常工作的调制解调器(比如传统的WinModem类设备)等。tty虚拟设备支持虚拟控制台,它能通过键盘及网络连接或者通过xterm会话登录到计算机上。
其实起初终端和控制台都不是个人电脑的概念,而是多人共用的小型中型大型计算机上的概念。终端为主机提供了人机接口,每个人都通过终端使用主机的资源。终端有字符终端和图形终端两种。一台主机可以连很多终端。控制台是一种特殊的人机接口, 是人控制主机的第一人机接口。
而主机对于控制台的信任度高于其他终端。对此还可以结合内核启动代码中init进程打开/dev/console和执行两次sys_dup(0),以及标准输入、标准输出、标准出错,还有就是进程fork后的标准输入输出的复制情况来一起理解。而个人计算机只有控制台,没有终端。当然愿意的话,可以在串口上连一两台字符哑终端。
但是linux按POSIX标准把个人计算机当成小型机来用,在控制台上通过getty软件虚拟了六个字符哑终端(或者叫虚拟控制台终端tty1-tty6)(数量可以在/etc/inittab里自己调整)和一个图型终端,在虚拟图形终端中又可以通过软件(如rxvt)再虚拟无限多个伪终端(pts/0等)。
但这全是虚拟的,虽然用起来一样,但实际上没有物理实体。所以在个人计算机上,只有一个实际的控制台,没有终端,所有终端都是在控制台上用软件模拟的。要把个人计算机当主机再通过串口或网卡外连真正的物理终端也可以,论成本,谁又会怎么做呢。
终端按照其自身能力分类,可以分为:
早期的计算机终端是通过串行RS-232通信的,它只能解释有限数量的控制码(CR,LF等),但没有能力处理执行特殊的转义序列功能(如清行、清屏或控制光标的位置)。简单来说就是处理能力有限的终端机,他们一般基本上只具有和机械电传打字机类似的有限功能。这种类型的终端称为哑终端。
现在仍然在现代类Unix系统上得到支持,通过设置环境变量TERM=dumb。哑终端有时用来指任何类型的通过RS-232连接的传统计算机终端,不对数据进行本地处理或本地执行用户程序的串行通信终端。哑终端有时也指功能有限,只有单色文本处理能力或直接传输每一个键入的字符而不等待主机轮询的公共计算机终端。
智能终端就是有能力处理转义序列,也就是说处理能力较强的终端机。
Linux系统的终端设备一般分为控制台、伪终端pty、串口终端(/dev/ttySn)和其它类型4种。
/dev/console是系统控制台,是与操作系统交互的设备。系统所产生的信息会发送到该设备上。平时我们看到的PC只有一个屏幕和键盘,它其实就是控制台。目前只有在单用户模式下,才允许用户登录控制台/dev/console。(可以在单用户模式下输入tty命令进行确认)。
console有缓冲的概念,为内核提供打印输出。内核把要打印的内容装入缓冲区__log_buff,然后由console来决定打印到哪里(比如是tty0还是ttySn等)。console指向激活的终端。历史上,console指主机本身的屏幕和键盘,而tty指用电缆链接的其它位置的控制台。
某些情况下console和tty0是一致的,就是当前所使用的是虚拟终端,也是激活虚拟终端。所以有些资料中称/dev/console是到/dev/tty0的符号链接,但是这样说现在看来是不对的:根据内核文档,在2.1.71之前,/dev/console根据不同系统设定,符号链接到/dev/tty0或者其他tty*上,在2.1.71版本之后则完全由内核代码内部控制它的映射。
如果一个终端设备要实现console功能,必须向内核注册一个struct console结构,一般的串口驱动中都会有。如果设备要实现tty功能,必须要向内核的tty子系统注册一个struct tty_driver结构,注册函数在drivers/tty/tty_io.c中。一个设备可以同时实现console和tty_driver,一般串口都这么做。
这是应用程序中的概念,如果当前进程有控制终端(Controlling Terminal),那么/dev/tty就是当前进程控制台的设备文件。对于你登录的shell,/dev/tty就是你使用的控制台,设备号是(5,0)。不过它并不指任何物理意义上的控制台,/dev/tty会映射到当前设备(使用命令tty
可以查看它具体对应哪个实际物理控制台设备)。输出到/dev/tty的内容只会显示在当前工作终端上(无论是登录在ttyn中还是pty中)。
你如果在控制台界面下(即字符界面下)那么dev/tty就是映射到dev/tty1-6之间的一个(取决于你当前的控制台号),但是如果你现在是在图形界面(Xwindows),那么你会发现现在的/dev/tty映射到的是/dev/pts的伪终端上。/dev/tty有些类似于到实际所使用终端设备的一个联接。
你可以输入命令tty
,显示当前映射终端如:/dev/tty1或者/dev/pts/0等。也可以使用命令ps -ax
来查看其他进程与哪个控制终端相连。
在当前终端中输入 echo “tekkaman” > /dev/tty
,都会直接显示在当前的终端中。
/dev/ttyn是进程虚拟控制台,他们共享同一个真实的物理控制台。如果在进程里打开一个这样的文件且该文件不是其他进程的控制台时,那该文件就是这个进程的控制台。
进程printf数据会输出到这里。在PC上,用户可以使用alt+Fn切换控制台,现在不知道怎么回事我用Ctrl + Alt + Fn才能切换。这没具体看过为啥。可能是Linux没有继承UNIX这方面的传统罢了。看起来感觉存在多个屏幕,这种虚拟控制台对应tty1~n,其中:/dev/tty1
代表第1个虚拟控制台;当使用ALT+F2进行切换时,系统的虚拟控制台为/dev/tty2
,当前控制台(/dev/tty)则指向/dev/tty2
在UNIX系统中,计算机显示器通常被称为控制台(console)。它仿真了类型为Linux的一种终端,并且有一些设备特殊文件与之相关联:tty0、tty1、tty2等。当你在控制台上登录时,使用的是tty1。使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。
读者可以登录到不同的虚拟控制台上去,因而可以让系统同时有几个不同的会话存在。
而比较特殊的是/dev/tty0,他代表当前虚拟控制台,其实就是当前所使用虚拟控制台的一个别名。因此不管当前正在使用哪个虚拟控制台(注意:这里是虚拟控制台,不包括伪终端),系统信息都会重定位到/dev/tty0上。
只有系统或超级用户root可以向/dev/tty0进行写操作。tty0是系统自动打开的,但不用于用户登录。在Framebuffer设备没有启用的系统中,可以使用/dev/tty0访问显卡。
伪终端(Pseudo Terminal)是终端的发展,为满足现在需求(比如网络登陆、xwindow窗口的管理)。它是成对出现的逻辑终端设备(即master和slave设备, 对master的操作会反映到slave上)。它多用于模拟终端程序,是远程登陆(telnet、ssh、xterm等)后创建的控制台设备。
简单说主终端和类似sshd,telnetd等用户空间的远程协议处理进程连接,而从终端则和shell之类的实际进程连接。在处理远程登录的时候,一般都是由远程协议处理进程打开主终端和从终端,然后就在远程网络终端和本机shell之间建立了一条双向通道(远程网络终端(套接字)<—>本机协议处理进程<—>主终端<—>从终端<—>shell)。
在这个“打开主从终端建立连接”的语义以及其实现上,有着不同的标准,总的来说有三种方式,分别是SVR4的方式,BSD的方式以及linux的方式。
在“建立连接”的语义上SVR4的方式使用“流”来建立这条连接,而BSD和linux则是自动建立的。
在“打开主从终端”的语义上,SVR4和linux是自动确定主终端并打开主终端后自动确定从终端,而BSD则必须手工确定和打开主终端。
可见linux处理伪终端的方式是结合SVR4和BSD两种UNIX标准的结果,linux不仅实现这种有意义的最佳组合,而且分别实现了SRV和BSD的两种方式的接口,如果编译CONFIG_LEGACY_PTYS宏,则可以使用BSD的方式,如果编译CONFIG_UNIX98_PTYS,则实现SRV4的接口。
由于可能有好几千个用户登陆,所以/dev/pts/* 是动态生成的,不象其他设备文件是构建系统时就已经产生的硬盘节点(如果未使用devfs、udev、mdev等) 。**第一个用户登陆,设备文件为/dev/pts/0,第二个为/dev/pts/1,以此类推。**它们并不与实际物理设备直接相关。现在大多数系统是通过此接口实现pty。
我们在X Window下打开的终端或使用telnet或ssh等方式登录Linux主机,此时均通过pty设备。例如,如果某人在网上使用telnet程序连接到你的计算机上,则telnet程序就可能会打开/dev/ptmx设备获取一个fd。此时一个getty程序就应该运行在对应的/dev/pts/* 上。当telnet从远端获取了一个字符时,该字符就会通过ptmx、pts/* 传递给 getty程序,而getty程序就会通过pts/* 、ptmx和telnet程序往网络上返回“login:”字符串信息。这样,登录程序与telnet程序就通过“伪终端”进行通信。
如果一个程序把 pts/* 看作是一个串行端口设备,则它对该端口的读/写操作会反映在该逻辑终端设备对的另一个/dev/ptmx上,而/dev/ptmx则是另一个程序用于读写操作的逻辑设备。这样,两个程序就可以通过这种逻辑设备进行互相交流,这很象是逻辑设备对之间的管道操作。对于pts/* ,任何设计成使用一个串行端口设备的程序都可以使用该逻辑设备。但对于使用/dev/ptmx的程序,则需要专门设计来使用/dev/ptmx逻辑设备。通过使用适当的软件,就可以把两个甚至多个伪终端设备连接到同一个物理串行端口上。
串行端口终端(Serial PortTerminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。有段时间串行端口设备通常被称为终端设备,那时它的最大用途就是用来连接终端,所以这些串行端口所对应的设备名称是/dev/tts/0(或/dev/ttyS0)、/dev/tts/1(或/dev /ttyS1)等,设备号分别是(4,0)、(4,1)等(对应于win系统下的COM1、COM2等)。若要向一个端口发送数据,可以在命令行上把标准输出重定向到这些特殊文件名上即可。
我们可以在命令行提示符下键入:echo "tekkaman" > /dev/ttyS1
会把“tekkaman”发送到连接在ttyS1(COM2)端口的设备上。
在2.6以后的内核后,一些三星的芯片将串口终端设备节点命名为ttySACn。TI的Omap系列芯片从2.6.37开始,芯片自带的UART设备开始使用专有的的omap-uart驱动,故设备节点命名为ttyOn,以区别于使用8250驱动时的设备名“ttySn”。这其中包括笔者用到的这个S3C2440。所以我们在Uboot启动参数中要设置console = ttySAC0才可以。这一句的意思其实就是把ttySAC0当做我们的控制台终端。
还针对很多不同的字符设备存在有很多其它种类的终端设备特殊文件,例如针对ISDN设备的**/dev/ttyIn**终端设备等。
在Linux kernel中,tty驱动不像于spi,iic等那么架构简单,它是一个庞大的系统,它的框架大体如下图一。我们作为普通的驱动开发移植人员,不会从零写tty驱动,一般都是厂家根据现有的tty驱动和自家芯片修改,拿到板子按照厂家的配置,串口应该使能直接使用的。但是开发的过程中也有可能需要用到串口,一般会修改serial驱动,这样我们不会动tty_core层。
Linux tty子系统包含:tty核心(tty_core),tty线路规程(tty_line_discipine)和tty驱动(tty_driver)。tty核心是对整个tty设备的抽象,对用户提供统一的接口,tty线路规程是对传输数据的格式化,tty驱动则是面向tty设备的硬件驱动。其整体框架如下图所示:
tty 核心从用户获取将要发送给 tty 设备的数据,接着传递它到 tty 线路规程驱动,再传递到 tty 驱动。 这个 tty 驱动转换数据为可以发送给硬件的格式。
从 tty 硬件收到的数据通过 tty 驱动向上回流,先进入 tty 线路规程驱动,再进入 tty 核心, 最后被用户获取。有时 tty 驱动直接和 tty 核心通讯,并且 tty 核心直接发送数据到 tty 驱动。但通常 tty 线路规程在它们 2 者之间修改数据格式以适配某种协议规则(蓝牙、PPP等)。
tty 核心提供一个非常容易的方式给任何 tty 驱动来维护一个文件在/proc/tty/driver
目录中。如果驱动定义了read_proc
或者 write_proc
函数,这个文件被创建。接着, 任何在这个文件上的读或写调用被发送给这个驱动.
这些函数的格式如同标准的 /proc 文件处理函数:
/* 打印出当前注册的端口号的read_proc函数示例*/
static int tiny_read_proc(char *page, char **start, off_t off, int count, int *eof, void *data)
{
struct tiny_serial *tiny;
off_t begin = 0;
int length = 0;
int i;
length += sprintf(page, "tinyserinfo:1.0 driver:%s\n", DRIVER_VERSION);
for (i = 0; i < TINY_TTY_MINORS && length < PAGE_SIZE; ++i)
{
tiny = tiny_table[i];
if (tiny == NULL)
continue;
length += sprintf(page+length, "%d\n", i);
if ((length + begin) > (off + count))
goto done;
if ((length + begin) < off) {
begin += length;
length = 0;
}
}
*eof = 1;
done:
if (off >= (length + begin))
return 0;
*start = page + (off-begin);
return (count < begin+length-off) ? count : begin + length-off;
}
当 tty 驱动被注册时, 或者当单个 tty 设备被创建时,tty 核心处理所有的 sysfs 目录和设备创建。 依赖在 struct tty_driver 中的TTY_DRIVER_NO_DEVFS 标志。单个目录一直包含 dev 文件, 它允许用户空间工具来决定分配给设备的主次号。
它还包含一个 device 和 driver 符号连接,如果一个指向有效的 struct device 的指针被传递给读 tty_register_device的调用。除了这 3 个文件, 对单个 tty 驱动不可能在这个位置创建新的sysfs 文件。这个会可能在将来的内核发行中改变。
线路(行)规程规定了键盘,串口,打印机,显示器等输入输出设备和用户态Shell等程序之间的行为规范,键盘上的按键事件被行规程解释成了Shell可以理解的输入并给出相应的输出。人们要想操作计算机,这套规程是必不可少的,它事实上规定了**信息从外部进入计算机的规范**。
当我们在一个终端上按下按键L的时候,终端只是把字母L回显了回来,紧接着按下按键S,依然是回显字母S,随后我们按下回车键,回显的不再是回车键,而是列出并显示了当前目录下的所有文件,这些规则就是线路规程。
可以使用
stty
命令来展示你的终端行规程的配置。
下图显示了Linux下线路规程在整个体系结构中的位置:
SLIP
SLIP就是**Serial Line Internet Protocol(串行线路网际协议)**可能现在很少有人再使用它了,毕竟现如今都是以太网和光纤的天下了,谁还会用串口线来传输网络数据包。但是它在以前很长一段时间一直作为连接运行TCP/IP协议的主机的专用链路存在的。
我们知道,TCP/IP是对等层通信协议,但是最终的数据包不得不通过某种物理介质传输,因此还需要一种纵向的协议才可以让对等层通信得以实现。我们把横向的对等层协议叫做**通信协议,而纵向的协议叫做传输协议,行规程事实上就是一种传输协议,SLIP实际上就是一种行规程**,SLIP行规程把串口线上传输的字符解释成IP数据报文并向上递送。这种行规程和TCP/IP的关系如下所示:
可以看得出,SLIP作为一中行规程,并没有把解析后的数据继续提交到与之绑定的TTY缓冲区,而是将解析后的数据作为IP数据报直接递送给了TCP/IP协议栈来处理。
广义地来讲,其实像以太网规范这种也可以叫做某种行规程,毕竟它也是约定IP层软件和传输介质之间行为规范的协议,起到的均是对数据格式化的作用,但由于如今超高速传输早就完完全全基于比特流序列,不再通过定义以字符为边界的块来封装数据,所以再叫做行规程就有点词不达意了,但本质上都是一样的,都是定义在某种介质上如何把数据包封装的协议。
串口、键盘和显示器在tty系统中的层次
应用程序通过open/read/write等标准接口访问硬件。引入行规程之后还要设置行规程,通过/ioctl或者其他封装好的函数去设置行规程。
有 3 种不同类型 tty 驱动: 控制台, 串口, 和 pty。控制台和 pty 驱动已经被编写进内核,所以任何使用 tty 核
心来与用户和系统交互的需要我们自己编写新驱动的只有串口驱动。
通过查看
/proc/tty/drivers
文件,可找到当前被加载到内核中的有哪些类型的 tty 驱动和设备。缺省的串口驱动创建一个文件在这个目录中来展示许多串口特定的硬件信息:驱动的名子 节点名子 主设备号 次设备号范围 驱动的类型
/dev/tty /dev/tty 5 0 system:/dev/tty
/dev/console /dev/console 5 1 system:console
/dev/ptmx /dev/ptmx 5 2 system
/dev/vc/0 /dev/vc/0 4 0 system:vtmaster
usbserial /dev/ttyUSB 188 0-254 serial
serial /dev/ttyS 4 64-67 serial
pty_slave /dev/pts 136 0-255 pty:slave
pty_master /dev/ptm 128 0-255 pty:master
通过查看
/sys/class/tty
文件,可找到所有当前注册到内核中的 tty 设备目录,目录下有一个 “dev” 文件包含有分配给那个 tty 设备的主次编号,和该驱动对应的设备及驱动的符号链接。/sys/class/tty/
|-- console
|-- dev |-- ptmx |
– dev
|-- tty
|-- dev |-- tty0 |
– dev
…|-- ttyUSB0
| |-- dev
| |-- device
-> …/…/…/devices/pci0000:00/0000:00:09.0/usb3/3-1/3-1:1.0/ttyUSB0
| `-- driver -> …/…/…/bus/usb-serial/drivers/keyspan_4
struct tty_driver定义
struct tty_driver {
int magic; //这个结构的"魔术"值. 应当一直设为 TTY_DRIVER_MAGIC
struct cdev cdev;
struct module *owner; //驱动的模块拥有者
const char *driver_name; //驱动的名子, 用在 /proc/tty 和 sysfs.
const char *name; //驱动的节点名
int name_base; //当创建设备名子时,使用的起始数字
int major; //驱动的主编号
int minor_start; //驱动的开始次编号. 这常常设为 name_base 的相同值
int minor_num; /* number of *possible* devices */
int num; /* number of devices allocated */
short type; /* type of tty driver */
short subtype; //其值依赖于type
struct ktermios init_termios; //当创建设备时的初始化 struct termios 值
int flags; /* tty driver flags */
int refcount; /* for loadable tty drivers */
struct proc_dir_entry *proc_entry; /* /proc fs entry ,它由 tty 核心创建如果驱动实现了
write_proc 或者 read_proc 函数 */
struct tty_driver *other; /* 指向一个 tty 从驱动. 这只被 pty 驱动使用, 并且不应当被其他的tty 驱动使用 */
/*
* Pointer to the tty data structures
*/
struct tty_struct **ttys;
struct ktermios **termios;
struct ktermios **termios_locked;
void *driver_state; /* tty 驱动的内部状态. 应当只被 pty 驱动使用 */
/*
* Interface routines from the upper tty layer to the tty
* driver. Will be replaced with struct tty_operations.
*/
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
int (*write)(struct tty_struct * tty, const unsigned char *buf, int count);
void (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty, struct file * file,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty, struct file * file,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
void (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
/*在对/proc/tty/driver目录中的文件的读写回调函数*/
int (*read_proc)(char *page, char **start, off_t off,
int count, int *eof, void *data);
int (*write_proc)(struct file *file, const char __user *buffer,
unsigned long count, void *data);
int (*tiocmget)(struct tty_struct *tty, struct file *file);
int (*tiocmset)(struct tty_struct *tty, struct file *file,
unsigned int set, unsigned int clear);
struct list_head tty_drivers;
};
owner:为了防止 tty 驱动在 tty 端口打开时被卸载,一般取值为THIS_MODULE
driver_name :驱动名,它出现在 /proc/tty/drivers
文件, 以及 /sys/class/tty 类目录下。
name:节点名,出现在/dev树中,通过在这个字串的后面追加在使用的 tty 设备号来创建多个 tty 设备。另外还在/sys/class/tty
目录中创建一个设备名子。如果 devfs 被使能,内核中的串口驱动设置这个 name 成员为 tts/
,如果没有使能,则为ttyS/
。
# 当driver_name = "tiny_tty"; name = "ttty"; devfs_name = "tts/ttty%d"; 时:
$ cat /proc/tty/drivers
tiny_tty /dev/ttty 240 0-3 serial
$ tree /sys/class/tty/ttty*
/sys/class/tty/ttty0
`-- dev
/sys/class/tty/ttty1
`-- dev
/sys/class/tty/ttty2
`-- dev
/sys/class/tty/ttty3
`-- dev
$ cat /sys/class/tty/ttty0/dev
240:0
major 变量描述这个驱动的主编号是什么
type、subtype :声明这个驱动是什么类型:
flags:指示驱动的当前状态和它是什么类型 tty 驱动。你必须使用的位掩码宏为:
自定义的tty设备结构体
struct tiny_serial
{
struct tty_struct *tty; /* pointer to the tty for this device */
int open_count; /* number of times this port has been opened */
struct semaphore sem; /* locks this structure */
struct timer_list *timer;
};
任何一个 tty 驱动的主要数据结构是 struct tty_driver。它用来注册和注销一个 tty 驱动到 tty 内核,在内核头文件
alloc_tty_driver()
/* allocate the tty driver */
tiny_tty_driver = alloc_tty_driver(TINY_TTY_MINORS);
if (!tiny_tty_driver)
return -ENOMEM;
tty_set_operation()
tiny_open()
{
...
}
tiny_close()
{
...
}
tiny_write()
{
...
}
tiny_write_room()
{
...
}
tiny_set_termios()
{
...
}
static struct tty_operations serial_ops = {
.open = tiny_open,
.close = tiny_close,
.write = tiny_write,
.write_room = tiny_write_room,
.set_termios = tiny_set_termios,
};
...
/* initialize the tty driver */
tiny_tty_driver->owner = THIS_MODULE;
tiny_tty_driver->driver_name = "tiny_tty";
tiny_tty_driver->name = "ttty";
tiny_tty_driver->devfs_name = "tts/ttty%d";
tiny_tty_driver->major = TINY_TTY_MAJOR;
tiny_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
tiny_tty_driver->subtype = SERIAL_TYPE_NORMAL;
tiny_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS;
tiny_tty_driver->init_termios = tty_std_termios;
tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
tty_set_operations(tiny_tty_driver, &serial_ops);
tty_operations
int (*open)(struct tty_struct * tty, struct file * filp); //open 函数.
void (*close)(struct tty_struct * tty, struct file * filp); //close 函数.
int (*write)(struct tty_struct * tty, const unsigned char *buf, int count);//write 函数.
/*
* 单字节写函数. 这个函数被 tty 核心调用当单个字节被写入设备. 如果
* 一个 tty 驱动没有定义这个函数, write 函数被调用来替代, 当 tty
* 核心想发送一个单个字节.
*/
void (*put_char)(struct tty_struct *tty, unsigned char ch);
/* 刷新数据到硬件的函数.*/
void (*flush_chars)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
int (*write_room)(struct tty_struct *tty); //指示多少缓冲空闲的函数
int (*chars_in_buffer)(struct tty_struct *tty); //指示多少缓冲满数据的函数.
int (*ioctl)(struct tty_struct *tty, struct file * file,
unsigned int cmd, unsigned long arg); //ioctl 函数
//当设备的 termios 设置已被改变时.这个函数被 tty 核心调用
void (*set_termios)(struct tty_struct *tty, struct termios * old);
/*
* 数据抑制函数. 这些函数用来帮助控制 tty 核心的输入缓存. 这个抑制
* 函数被调用当 tty 核心的输入缓冲满. tty 驱动应当试图通知设备不应
* 当发送字符给它. unthrottle 函数被调用当 tty 核心的输入缓冲已被
* 清空, 并且它现在可以接收更多数据. tty 驱动应当接着通知设备可以
* 接收数据. stop 和 start 函数非常象 throttle 和 unthrottle 函数,
* 但是它们表示 tty 驱动应当停止发送数据给设备以及以后恢复发送数据.
*/
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty); //挂起函数
/*
* 线路中断控制函数. 当这个 tty 驱动在 RS-232 端口上要打开或关闭线路的 BREAK 状态,
* 这个函数被调用. 如果状态设为 -1, BREAK 状态应当打开. 如果状态设为 0, BREAK 状态应当关闭.
* 如果这个函数由 tty 驱动实现, tty 核心将处理 TCSBRK, TCSBRKP, TIOCSBRK, 和 TIOCCBRK
* ioctl. 否则, 这些 ioctls 被发送给驱动 ioctl 函数.
*/
void (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty); //刷新缓冲和丢失任何剩下的数据.
void (*set_ldisc)(struct tty_struct *tty); //设置线路规程的函数. 当 tty 核心已改变这个 tty 驱动的线路规程,这个函数被调用. 它通常不用并且不应当被一个驱动定义.
/*
* 发送 X-类型 字符 的函数. 这个函数用来发送一个高优先级 XON 或者
* XOFF 字符给 tty 设备. 要被发送的字符在 ch 变量中指定.
*/
void (*send_xchar)(struct tty_struct *tty, char ch);
/* proc 读和写函数.*/
int (*read_proc)(char *page, char **start, off_t off, int count, int *eof, void data);
int (*write_proc)(struct file *file, const char *buffer, unsigned long count, void *data);
int (*tiocmget)(struct tty_struct *tty, struct file *file); //获得当前的特定 tty 设备的线路设置.
/* 设置当前的特定 tty 设备的线路设置. set 和 clear 包含了去设置或者清除的不同的线路设置 */
int (*tiocmset)(struct tty_struct *tty, struct file *file, unsigned int set, unsigned int clear);
tty_register_driver() / tty_unregister_driver()
/* register the tty driver */
retval = tty_register_driver(tiny_tty_driver);
if (retval)
{
printk(KERN_ERR "failed to register tiny tty driver");
put_tty_driver(tiny_tty_driver);
return retval;
}
/* unregister the tty driver */
tty_unregister_driver(tiny_tty_driver);
tty_register_device() / tty_unregister_device()/
/* register the tty devices */
for (i = 0; i < TINY_TTY_MINORS; ++i)
tty_register_device(tiny_tty_driver, i, NULL);
/* unregister the tty devices */
for (i = 0; i < TINY_TTY_MINORS; ++i)
tty_unregister_device(tiny_tty_driver, i);
这个 struct termios 结构用来描述某个 tty 设备的一个特定端口当前的线路规程设置(波特率, 数据大小, 数据流控…)。在tty_driver结构体中的init_termios变量就是该结构类型,用于描述一个完全的线路规程设置,且驱动初始化时会调用一个标准的数值集合(tty_std_termios)来初始化该init_termios变量。
struct termios tty_std_termios = {
.c_iflag = ICRNL | IXON, //输入模式标志
.c_oflag = OPOST | ONLCR, //输出模式标志
.c_cflag = B38400 | CS8 | CREAD | HUPCL, //控制模式标志
.c_lflag = ISIG | ICANON | ECHO | ECHOE | //本地模式标志
ECHOK | ECHOCTL | ECHOKE | IEXTEN,
.c_cc = INIT_C_CC //一个控制字符数组
};
当一个用户要改变一个 tty 设备的线路设置或者获取当前线路设置时,要么调用用户空间下的termios 系列库函数,要么直接对节点调用ioctl。tty核心将自动为大部分 termios 用户空间函数转换为一个对驱动节点的 ioctl 调用。大量的不同的 tty ioctl 调用接着被 tty 核心转换为一个对 tty 驱动的单个set_termios 函数调用。set_termios 调用需要决定哪个线路设置它被请求来改变, 接着在 tty 设备中做这些改变。
set_termios
一个 set_termios 函数应当做的第一件事情是决定任何事情是否真的需要改变,可使用下面的代码完成 :
#define RELEVANT_IFLAG(iflag) ((iflag) & (IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK))
unsigned int cflag;
cflag = tty->termios->c_cflag;
/* check that they really want us to change something */
if (old_termios) //old_termios 变量在它被存取之前,需要被检查是否指向一个有效的结构
{
if ((cflag == old_termios->c_cflag) &&
(RELEVANT_IFLAG(tty->termios->c_iflag) == RELEVANT_IFLAG(old_termios->c_iflag))) {
printk(KERN_DEBUG " - nothing to change...\n");
return;
}
}
/* 为查看需要的字节大小, CSIZE 位掩码可用来从 cflag 变量区分出正确的位
* PARODD 位掩码可用来决定是否奇偶应当是奇或者偶
*/
switch (cflag & CSIZE)
{
case CS5:
printk(KERN_DEBUG " - data bits = 5\n");
break;
case CS6:
printk(KERN_DEBUG " - data bits = 6\n");
break;
case CS7:
printk(KERN_DEBUG " - data bits = 7\n");
break;
default:
case CS8:
printk(KERN_DEBUG " - data bits = 8\n");
break;
}
/* 为决定需要的奇偶值, PARENB 位掩码可对 cflag 变量检查来告知是否任何奇偶要被设置. */
if (cflag & PARENB)
if (cflag & PARODD)
printk(KERN_DEBUG " - parity = odd\n");
else
printk(KERN_DEBUG " - parity = even\n");
else
printk(KERN_DEBUG " - parity = none\n");
/* 请求的停止位也可使用 CSTOPB 位掩码从 cflag 变量中来知道 */
if (cflag & CSTOPB)
printk(KERN_DEBUG " - stop bits = 2\n");
else
printk(KERN_DEBUG " - stop bits = 1\n");
/* 为确定是否用户要求硬件流控,CRTSCTS 位掩码用来对 cflag 变量检查 */
if (cflag & CRTSCTS)
printk(KERN_DEBUG " - RTS/CTS is enabled\n");
else
printk(KERN_DEBUG " - RTS/CTS is disabled\n");
/* 确定软件流控的不同模式和不同的起停字符是有些复杂 */
if (I_IXOFF(tty) || I_IXON(tty))
{
unsigned char stop_char = STOP_CHAR(tty);
unsigned char start_char = START_CHAR(tty);
/* if we are implementing INBOUND XON/XOFF */
if (I_IXOFF(tty))
printk(KERN_DEBUG " - INBOUND XON/XOFF is enabled, "
"XON = %2x, XOFF = %2x", start_char, stop_char);
else
printk(KERN_DEBUG" - INBOUND XON/XOFF is disabled");
/* if we are implementing OUTBOUND XON/XOFF */
if (I_IXON(tty))
printk(KERN_DEBUG" - OUTBOUND XON/XOFF is enabled, "
"XON = %2x, XOFF = %2x", start_char, stop_char);
else
printk(KERN_DEBUG" - OUTBOUND XON/XOFF is disabled");
}
/* 最后, 波特率需要确定. tty 核心提供了一个函数, tty_get_baud_rate, 来帮助做这个 */
printk(KERN_DEBUG " - baud rate = %d", tty_get_baud_rate(tty));
tiocmget
当核心需要知道当前的特定tty 设备的控制线的物理值(串口的DTR和RTS线的值),tty 驱动中的 iocmget 函数被 tty 核心所调用。
如果 tty 驱动不能直接读串口的 MSR 或者 MCR 寄存器, 一个它们的拷贝应当在本地保持. 许多 USB-到-串口 驱动必须实现这类的"影子"变量.
static int tiny_tiocmget(struct tty_struct *tty, struct file *file)
{
struct tiny_serial *tiny = tty->driver_ data;
unsigned int result = 0;
unsigned int msr = tiny->msr;
unsigned int mcr = tiny->mcr;
result = ((mcr & MCR_DTR) ? TIOCM_DTR : 0) | /* DTR is set */
((mcr & MCR_RTS) ? TIOCM_RTS : 0 ) | /* RTS is set */
((mcr & MCR_LOOP) ? TIOCM_LOOP : 0) | /* LOOP is set */
((msr & MSR_CTS) ? TIOCM_CTS : 0) | /* CTS is set */
((msr & MSR_CD) ? TIOCM_CAR : 0)| /* Carrier detect is set */
((msr & MSR_RI) ? TIOCM_RI : 0) | /* Ring Indicatoris set */
((msr & MSR_DSR) ? TIOCM_DSR : 0); /* DSR is set */
return result;
}
tiocmset
当核心要设置一个特定tty 设备的控制线值,tty 驱动中的 tiocmset 函数被 tty 核心调用。
tty 核心通过传递 set 和 clear 告知 tty 驱动设置什么值和清理什么 ,这些变量包含一个应当改变的线路设置的位掩码。
static int tiny_tiocmset(struct tty_struct *tty, struct file *file,
unsigned int set , unsigned int clear)
{
struct tiny_serial *tiny = tty->driver_data;
unsigned int mcr = tiny->mcr;
if (set & TIOCM_RTS)
mcr |= MCR_RTS;
if (set & TIOCM_DTR)
mcr |= MCR_RTS;
if (clear & TIOCM_RTS)
mcr &= ~MCR_RTS;
if (clear & TIOCM_DTR)
mcr &= ~MCR_RTS;
/* set the new MCR value in the device */
tiny->mcr = mcr;
return 0;
}
当 ioctl() 被在设备节点上调用,struct tty_driver 中的 ioctl 函数被 tty 核心调用。如果这个 tty 驱动不知道如何处理传递给它的 ioctl 值, 它应当返回 -ENOIOCTLCMD 来试图让 tty 核心实现一个通用的调用版本。
2.6 内核定义了大约 70 个不同的 tty ioctls,大部分的 tty 驱动不全部处理它们, 但是处理一个小的子集。下面讲述一个更通用的 tty ioctls 列表,以及如何实现它们:
cmd | 涵义 |
---|---|
TIOCSERGETLSR | 获得 tty 设备的线路状态寄存器( LSR )的值 |
TIOCGSERIAL | 获得串口线信息,调用者传递一个指向一个大的 serial_struct 结构的指针,这个结构应当由 tty 驱动填充正确的值 |
TIOCSSERIAL | 设置串口线路信息,一个指向 struct serial_struct 的指针被传递给这个调用 |
TIOCMIWAIT | 等待 MSR 改变 |
TIOCGICOUNT | 中断计数(已经产生多少串口线中断) |
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg)
{
struct tiny_serial *tiny = tty->driver_data;
//获得串口线信息
if (cmd == TIOCGSERIAL)
{
struct serial_struct tmp;
if (!arg)
return -EFAULT;
memset(&tmp, 0, sizeof(tmp));
tmp.type = tiny->serial.type;
tmp.line = tiny->serial.line;
tmp.port = tiny->serial.port;
tmp.irq = tiny->serial.irq;
tmp.flags = ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ;
tmp.xmit_fifo_size = tiny->serial.xmit_fifo_size;
tmp.baud_base = tiny->serial.baud_base;
tmp.close_delay = 5*HZ;
tmp.closing_wait = 30*HZ;
tmp.custom_divisor = tiny->serial.custom_divisor;
tmp.hub6 = tiny->serial.hub6;
tmp.io_type = tiny->serial.io_type;
if (copy_to_user((void __user *)arg, &tmp, sizeof(tmp)))
return -EFAULT;
return 0;
}
/* arg 参数包含用户在等待的事件类型,当实现这个ioctl时不要使用interruptible_sleep_on调用.
* 在 tty 驱动的代码中能知道 MSR 寄存器改变的某些地方,
* 必须调用wake_up_interruptible(&tp->wait);以便这个代码能正常工作:
*/
if (cmd == TIOCMIWAIT)
{
DECLARE_WAITQUEUE(wait, current);
struct async_icount cnow;
struct async_icount cprev;
cprev = tiny->icount;
while (1) {
add_wait_queue(&tiny->wait, &wait);
set_current_state(TASK_INTERRUPTIBLE);
schedule();
remove_wait_queue(&tiny->wait, &wait);
/* see if a signal woke us up */
if (signal_pending(current))
return -ERESTARTSYS;
cnow = tiny->icount;
if (cnow.rng == cprev.rng &&
cnow.dsr == cprev.dsr &&
cnow.dcd == cprev.dcd &&
cnow.cts == cprev.cts)
{
return -EIO; /* no change => error */
}
if(((arg & TIOCM_RNG) && (cnow.rng !=cprev.rng)) ||
((arg & TIOCM_DSR) && (cnow.dsr != cprev.dsr))||
((arg & TIOCM_CD) && (cnow.dcd != cprev.dcd)) ||
((arg & TIOCM_CTS)&& (cnow.cts != cprev.cts)) )
{
return 0;
}
cprev = cnow;
}
}
/* 这个调用通过arg向内核传递一个serial_icounter_struct 的指针,常常和之前的 IOCMIWAIT ioctl 调用结合使用 */
if (cmd == TIOCGICOUNT)
{
struct async_icount cnow = tiny->icount;
struct serial_icounter_struct icount;
icount.cts = cnow.cts;
icount.dsr = cnow.dsr;
icount.rng = cnow.rng;
icount.dcd = cnow.dcd;
icount.rx = cnow.rx;
icount.tx = cnow.tx;
icount.frame = cnow.frame;
icount.overrun = cnow.overrun;
icount.parity = cnow.parity;
icount.brk = cnow.brk;
icount.buf_overrun = cnow.buf_overrun;
if (copy_to_user((void __user *)arg, &icount, sizeof(icount)))
return -EFAULT;
return 0;
}
return -ENOIOCTLCMD;
}
当一个用户对某个 tty 设备节点调用 open 时, tty 核心调用tty_operationsopen 中注册的open函数。该函数使用tty_struct和file指针作为参数,并将一些数据(可以基于端口次编号来引用的静态数组中的数据 )到tty_struct中,以便被用于后续的close, write 使用。下面的例子显示了tiny_tty 驱动中的open函数保存一个指针(用户自定义设备结构体tiny)到 tty 结构中。
static int tiny_open(struct tty_struct *tty, struct file *file)
{
struct tiny_serial *tiny; //用户自定义的设备结构体tiny_serial
struct timer_list *timer;
int index;
/* initialize the pointer in case something fails */
tty->driver_data = NULL;
/* get the serial object associated with this tty pointer */
index = tty->index;
tiny = tiny_table[index];
if (tiny == NULL)
{
/* first time accessing this device, let's create it */
tiny = kmalloc(sizeof(*tiny), GFP_KERNEL);
if (!tiny)
return -ENOMEM;
init_MUTEX(&tiny->sem);
tiny->open_count = 0;
tiny->timer = NULL;
tiny_table[index] = tiny;
}
down(&tiny->sem);
/* save our structure within the tty structure */
/* tiny_serial 结构被保存在 tty 结构中.
* 允许 tiny_write,tiny_write_room, 和 tiny_close 函数来获取 tiny_serial 结构和正确操作它
*/
tiny->tty = tty;
tty->driver_data = tiny;
/* trace the tty device opened times */
++tiny->open_count;
if (tiny->open_count == 1)
{
/* 若设备是第一次被打开, 还需要做任何必要的硬件初始化和内存分配工作 */
...
}
return 0;
}
用户对前面使用 open 调用而创建的文件句柄调用 close 时,tty 核心调用tty_operationsopen 中注册的close函数。因为 open 函数可被多次调用, close 函数也可多次调用. 因此这个函数应当跟踪它被调用的次数来决定是否硬件应当在此次真正被关闭。
static void do_close(struct tiny_serial *tiny)
{
down(&tiny->sem);
if (!tiny->open_count)
{
/* port was never opened */
goto exit;
}
--tiny->open_count;
if (tiny->open_count <= 0)
{
/* The port is being closed by the last user. */
/* Do any hardware specific stuff here */
...
/* shut down our timer */
del_timer(tiny->timer);
}
exit:
up(&tiny->sem);
}
static void tiny_close(struct tty_struct *tty, struct file *file)
{
struct tiny_serial *tiny = tty->driver_data;
if (tiny)
do_close(tiny);
}
用户在有数据发送给硬件时,write 函数被调用。但不是所有的写程序要求的字符都可以在调用写函数时被发送,因为还需要考虑速度匹配和 tty 硬件的缓冲区容量大小。函数返回能够发送给硬件的字符数,如果发生写错误则返回负的错误码。
write 函数被要求可在中断上下文和用户上下文中被调用。所以tty驱动不应当调用任何可引起睡眠的函数(copy_from_user、kmalloc、printk等)。内核提供了一个函数calling_in_interrupt
来判断驱动是否处于在中断上下文中。
下面的代码显示了一个简单的写函数,因为没有实际的tty硬件,故我们将要写的数据记录到内核调试日志中
static int tiny_write(struct tty_struct *tty, const unsigned char *buffer, int count)
{
struct tiny_serial *tiny = tty->driver_data;
int i;
int ret = -EINVAL;
if (!tiny)
return -ENODEV;
down(&tiny->sem);
if (!tiny->open_count)
/* port was not opened */
goto exit;
/* fake sending the data out a hardware port by
* writing it to the kernel debug log.
*/
printk(KERN_DEBUG "%s - ", __FUNCTION__);
for (i = 0; i < count; ++i)
printk("%02x ", buffer[i]);
printk("\n");
exit:
up(&tiny->sem);
return ret;
}
如果tty 驱动在 tty_struct 中没有实现 put_char 函数,tty 核心用一个数据大小为 1 来使用 write 函数回调。这普遍发生在tty 核心想转换一个新行字符为一个换行,此时write函数不得将数据进行缓冲和重试。但因为 write 函数不知道是否它在被调用来替代 put_char,因此,一些终端类型(例如很多USB-到-串口 )不能正确工作。
write_room()
当 tty 核心想知道多少空间在写缓冲中时,可调用此函数。随着字符不断从写缓冲清空以及调用写函数时添加字符到写缓冲,这个数字时时改变的。
static int tiny_write_room(struct tty_struct *tty)
{
struct tiny_serial *tiny = tty->driver_data;
int room = -EINVAL;
if (!tiny)
return -ENODEV;
down(&tiny->sem);
if (!tiny->open_count)
{
/* port was not opened */
goto exit;
}
/* calculate how much room is left in the device */
...
exit:
up(&tiny->sem);
return room;
}
tty 核心提供一个缓冲逻辑用来存放从硬件收到的数据,但该缓冲区溢出时,并不会通知tty核心或用户。该缓冲区用 struct tty_flip_buffer
来表示。
一个 flip 缓冲是一个包含 2 个主要数据数组的结构. 从 tty 设备接收到的数据被存储于第一个数组. 当这个数组满, 任何等待数据的用户被通知数据可以读. 当用户从这个数组读数据, 任何新到的数据被存储在第 2 个数组。当那个数组被读空, 数据再次刷新给用户, 并且驱动开始填充第 1 个数组。
为试图阻止数据丢失, 一个 tty 驱动可以监视到来的数组多大,并且,如果它添满了,要及时告知 tty 驱动刷新缓冲,而不是等待下一个可用的机会。
tty 驱动 只需要关心struct tty_flip_buffer 结构中的 cout变量,它表示当前缓冲里还可用来接收数据的空间大小。如果这个值等于值 TTY_FLIPBUF_SIZE,这个 flip 缓冲需要被刷新到用户。一个使用cout变量的例子如下:
for (i = 0; i < data_size; ++i)
{
if (tty->flip.count >= TTY_FLIPBUF_SIZE)
tty_flip_buffer_push(tty);
/*将从 tty 驱动接收来的要发送给用户的字符添加到 flip 缓冲*/
tty_insert_flip_char(tty, data[i], TTY_NORMAL);
}
tty_flip_buffer_push(tty);
tty_insert_flip_char()
tty_flip_buffer_push()
tty_struct 变量被 tty 核心用来保持当前的特定 tty 端口的状态。
/* */
struct tty_struct {
int magic;
struct tty_driver *driver; //当前控制这个 tty 设备的 tty_driver 结构
int index;
struct tty_ldisc ldisc; //给 tty 设备的线路规程
struct mutex termios_mutex;
struct ktermios *termios, *termios_locked; //指向 tty 设备的当前 termios 设置的指针
char name[64];
struct pid *pgrp;
struct pid *session;
unsigned long flags; //tty 设备的当前状态
int count;
struct winsize winsize;
/*
* stopped: 指示tty 设备是否 被停止. tty 驱动可以设置这个值
* hw_stopped:指示tty 设备是否 已经停止. tty 驱动可以设置这个值
* low_latency:指示tty 设备是否能够高速接收数据. tty驱动可以设置这个值.
*/
unsigned char stopped:1, hw_stopped:1, flow_stopped:1, packet:1;
unsigned char low_latency:1, warned:1;
unsigned char ctrl_status;
unsigned int receive_room; /* Bytes free for queue */
struct tty_struct *link;
struct fasync_struct *fasync;
struct tty_bufhead buf;
int alt_speed; /* For magic substitution of 38400 bps */
wait_queue_head_t write_wait; //当tty驱动可以接收更多数据时,应当唤醒它
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data; //用来存储对于 tty 驱动本地的数据. 这个变量不被 tty 核心修改.
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
/*
* The following is data for the N_TTY line discipline. For
* historical reasons, this is included in the tty structure.
*/
unsigned int column;
unsigned char lnext:1, erasing:1, raw:1, real_raw:1, icanon:1;
unsigned char closing:1; //指示tty 设备是否在关闭端口当中. tty 驱动可以设置这个值
unsigned short minimum_to_wake;
unsigned long overrun_time;
int num_overrun;
unsigned long process_char_map[256/(8*sizeof(unsigned long))];
char *read_buf;
int read_head;
int read_tail;
int read_cnt;
unsigned long read_flags[N_TTY_BUF_SIZE/(8*sizeof(unsigned long))];
int canon_data;
unsigned long canon_head;
unsigned int canon_column;
struct mutex atomic_read_lock;
struct mutex atomic_write_lock;
unsigned char *write_buf;
int write_cnt;
spinlock_t read_lock;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
};
几乎它的所有条目都只被 tty 核心使用,只有几个例外是被tty驱动使用的,下面一一讲解
/*
* Tiny TTY driver
*
* This driver shows how to create a minimal tty driver. It does not rely on
* any backing hardware, but creates a timer that emulates data being received
* from some kind of hardware.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DRIVER_VERSION "v2.0"
#define DRIVER_AUTHOR "Greg Kroah-Hartman "
#define DRIVER_DESC "Tiny TTY driver"
/* Module information */
MODULE_AUTHOR( DRIVER_AUTHOR );
MODULE_DESCRIPTION( DRIVER_DESC );
MODULE_LICENSE("GPL");
#define DELAY_TIME HZ * 2 /* 2 seconds per character */
#define TINY_DATA_CHARACTER 't'
#define TINY_TTY_MAJOR 240 /* experimental range */
#define TINY_TTY_MINORS 4 /* only have 4 devices */
struct tiny_serial {
struct tty_struct *tty; /* pointer to the tty for this device */
int open_count; /* number of times this port has been opened */
struct semaphore sem; /* locks this structure */
struct timer_list *timer;
/* for tiocmget and tiocmset functions */
int msr; /* MSR shadow */
int mcr; /* MCR shadow */
/* for ioctl fun */
struct serial_struct serial;
wait_queue_head_t wait;
struct async_icount icount;
};
static struct tiny_serial *tiny_table[TINY_TTY_MINORS]; /* initially all NULL */
static void tiny_timer(unsigned long timer_data)
{
struct tiny_serial *tiny = (struct tiny_serial *)timer_data;
struct tty_struct *tty;
int i;
char data[1] = {TINY_DATA_CHARACTER};
int data_size = 1;
if (!tiny)
return;
tty = tiny->tty;
/* send the data to the tty layer for users to read. This doesn't
* actually push the data through unless tty->low_latency is set */
for (i = 0; i < data_size; ++i) {
if (tty->flip.count >= TTY_FLIPBUF_SIZE)
tty_flip_buffer_push(tty);
tty_insert_flip_char(tty, data[i], TTY_NORMAL);
}
tty_flip_buffer_push(tty);
/* resubmit the timer again */
tiny->timer->expires = jiffies + DELAY_TIME;
add_timer(tiny->timer);
}
static int tiny_open(struct tty_struct *tty, struct file *file)
{
struct tiny_serial *tiny;
struct timer_list *timer;
int index;
/* initialize the pointer in case something fails */
tty->driver_data = NULL;
/* get the serial object associated with this tty pointer */
index = tty->index;
tiny = tiny_table[index];
if (tiny == NULL) {
/* first time accessing this device, let's create it */
tiny = kmalloc(sizeof(*tiny), GFP_KERNEL);
if (!tiny)
return -ENOMEM;
init_MUTEX(&tiny->sem);
tiny->open_count = 0;
tiny->timer = NULL;
tiny_table[index] = tiny;
}
down(&tiny->sem);
/* save our structure within the tty structure */
tty->driver_data = tiny;
tiny->tty = tty;
++tiny->open_count;
if (tiny->open_count == 1) {
/* this is the first time this port is opened */
/* do any hardware initialization needed here */
/* create our timer and submit it */
if (!tiny->timer) {
timer = kmalloc(sizeof(*timer), GFP_KERNEL);
if (!timer) {
up(&tiny->sem);
return -ENOMEM;
}
tiny->timer = timer;
}
tiny->timer->data = (unsigned long )tiny;
tiny->timer->expires = jiffies + DELAY_TIME;
tiny->timer->function = tiny_timer;
add_timer(tiny->timer);
}
up(&tiny->sem);
return 0;
}
static void do_close(struct tiny_serial *tiny)
{
down(&tiny->sem);
if (!tiny->open_count) {
/* port was never opened */
goto exit;
}
--tiny->open_count;
if (tiny->open_count <= 0) {
/* The port is being closed by the last user. */
/* Do any hardware specific stuff here */
/* shut down our timer */
del_timer(tiny->timer);
}
exit:
up(&tiny->sem);
}
static void tiny_close(struct tty_struct *tty, struct file *file)
{
struct tiny_serial *tiny = tty->driver_data;
if (tiny)
do_close(tiny);
}
static int tiny_write(struct tty_struct *tty,
const unsigned char *buffer, int count)
{
struct tiny_serial *tiny = tty->driver_data;
int i;
int retval = -EINVAL;
if (!tiny)
return -ENODEV;
down(&tiny->sem);
if (!tiny->open_count)
/* port was not opened */
goto exit;
/* fake sending the data out a hardware port by
* writing it to the kernel debug log.
*/
printk(KERN_DEBUG "%s - ", __FUNCTION__);
for (i = 0; i < count; ++i)
printk("%02x ", buffer[i]);
printk("\n");
exit:
up(&tiny->sem);
return retval;
}
static int tiny_write_room(struct tty_struct *tty)
{
struct tiny_serial *tiny = tty->driver_data;
int room = -EINVAL;
if (!tiny)
return -ENODEV;
down(&tiny->sem);
if (!tiny->open_count) {
/* port was not opened */
goto exit;
}
/* calculate how much room is left in the device */
room = 255;
exit:
up(&tiny->sem);
return room;
}
#define RELEVANT_IFLAG(iflag) ((iflag) & (IGNBRK|BRKINT|IGNPAR|PARMRK|INPCK))
static void tiny_set_termios(struct tty_struct *tty, struct termios *old_termios)
{
unsigned int cflag;
cflag = tty->termios->c_cflag;
/* check that they really want us to change something */
if (old_termios) {
if ((cflag == old_termios->c_cflag) &&
(RELEVANT_IFLAG(tty->termios->c_iflag) ==
RELEVANT_IFLAG(old_termios->c_iflag))) {
printk(KERN_DEBUG " - nothing to change...\n");
return;
}
}
/* get the byte size */
switch (cflag & CSIZE) {
case CS5:
printk(KERN_DEBUG " - data bits = 5\n");
break;
case CS6:
printk(KERN_DEBUG " - data bits = 6\n");
break;
case CS7:
printk(KERN_DEBUG " - data bits = 7\n");
break;
default:
case CS8:
printk(KERN_DEBUG " - data bits = 8\n");
break;
}
/* determine the parity */
if (cflag & PARENB)
if (cflag & PARODD)
printk(KERN_DEBUG " - parity = odd\n");
else
printk(KERN_DEBUG " - parity = even\n");
else
printk(KERN_DEBUG " - parity = none\n");
/* figure out the stop bits requested */
if (cflag & CSTOPB)
printk(KERN_DEBUG " - stop bits = 2\n");
else
printk(KERN_DEBUG " - stop bits = 1\n");
/* figure out the hardware flow control settings */
if (cflag & CRTSCTS)
printk(KERN_DEBUG " - RTS/CTS is enabled\n");
else
printk(KERN_DEBUG " - RTS/CTS is disabled\n");
/* determine software flow control */
/* if we are implementing XON/XOFF, set the start and
* stop character in the device */
if (I_IXOFF(tty) || I_IXON(tty)) {
unsigned char stop_char = STOP_CHAR(tty);
unsigned char start_char = START_CHAR(tty);
/* if we are implementing INBOUND XON/XOFF */
if (I_IXOFF(tty))
printk(KERN_DEBUG " - INBOUND XON/XOFF is enabled, "
"XON = %2x, XOFF = %2x", start_char, stop_char);
else
printk(KERN_DEBUG" - INBOUND XON/XOFF is disabled");
/* if we are implementing OUTBOUND XON/XOFF */
if (I_IXON(tty))
printk(KERN_DEBUG" - OUTBOUND XON/XOFF is enabled, "
"XON = %2x, XOFF = %2x", start_char, stop_char);
else
printk(KERN_DEBUG" - OUTBOUND XON/XOFF is disabled");
}
/* get the baud rate wanted */
printk(KERN_DEBUG " - baud rate = %d", tty_get_baud_rate(tty));
}
/* Our fake UART values */
#define MCR_DTR 0x01
#define MCR_RTS 0x02
#define MCR_LOOP 0x04
#define MSR_CTS 0x08
#define MSR_CD 0x10
#define MSR_RI 0x20
#define MSR_DSR 0x40
static int tiny_tiocmget(struct tty_struct *tty, struct file *file)
{
struct tiny_serial *tiny = tty->driver_data;
unsigned int result = 0;
unsigned int msr = tiny->msr;
unsigned int mcr = tiny->mcr;
result = ((mcr & MCR_DTR) ? TIOCM_DTR : 0) | /* DTR is set */
((mcr & MCR_RTS) ? TIOCM_RTS : 0) | /* RTS is set */
((mcr & MCR_LOOP) ? TIOCM_LOOP : 0) | /* LOOP is set */
((msr & MSR_CTS) ? TIOCM_CTS : 0) | /* CTS is set */
((msr & MSR_CD) ? TIOCM_CAR : 0) | /* Carrier detect is set*/
((msr & MSR_RI) ? TIOCM_RI : 0) | /* Ring Indicator is set */
((msr & MSR_DSR) ? TIOCM_DSR : 0); /* DSR is set */
return result;
}
static int tiny_tiocmset(struct tty_struct *tty, struct file *file,
unsigned int set, unsigned int clear)
{
struct tiny_serial *tiny = tty->driver_data;
unsigned int mcr = tiny->mcr;
if (set & TIOCM_RTS)
mcr |= MCR_RTS;
if (set & TIOCM_DTR)
mcr |= MCR_RTS;
if (clear & TIOCM_RTS)
mcr &= ~MCR_RTS;
if (clear & TIOCM_DTR)
mcr &= ~MCR_RTS;
/* set the new MCR value in the device */
tiny->mcr = mcr;
return 0;
}
static int tiny_read_proc(char *page, char **start, off_t off, int count,
int *eof, void *data)
{
struct tiny_serial *tiny;
off_t begin = 0;
int length = 0;
int i;
length += sprintf(page, "tinyserinfo:1.0 driver:%s\n", DRIVER_VERSION);
for (i = 0; i < TINY_TTY_MINORS && length < PAGE_SIZE; ++i) {
tiny = tiny_table[i];
if (tiny == NULL)
continue;
length += sprintf(page+length, "%d\n", i);
if ((length + begin) > (off + count))
goto done;
if ((length + begin) < off) {
begin += length;
length = 0;
}
}
*eof = 1;
done:
if (off >= (length + begin))
return 0;
*start = page + (off-begin);
return (count < begin+length-off) ? count : begin + length-off;
}
#define tiny_ioctl tiny_ioctl_tiocgserial
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg)
{
struct tiny_serial *tiny = tty->driver_data;
if (cmd == TIOCGSERIAL) {
struct serial_struct tmp;
if (!arg)
return -EFAULT;
memset(&tmp, 0, sizeof(tmp));
tmp.type = tiny->serial.type;
tmp.line = tiny->serial.line;
tmp.port = tiny->serial.port;
tmp.irq = tiny->serial.irq;
tmp.flags = ASYNC_SKIP_TEST | ASYNC_AUTO_IRQ;
tmp.xmit_fifo_size = tiny->serial.xmit_fifo_size;
tmp.baud_base = tiny->serial.baud_base;
tmp.close_delay = 5*HZ;
tmp.closing_wait = 30*HZ;
tmp.custom_divisor = tiny->serial.custom_divisor;
tmp.hub6 = tiny->serial.hub6;
tmp.io_type = tiny->serial.io_type;
if (copy_to_user((void __user *)arg, &tmp, sizeof(struct serial_struct)))
return -EFAULT;
return 0;
}
return -ENOIOCTLCMD;
}
#undef tiny_ioctl
#define tiny_ioctl tiny_ioctl_tiocmiwait
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg)
{
struct tiny_serial *tiny = tty->driver_data;
if (cmd == TIOCMIWAIT) {
DECLARE_WAITQUEUE(wait, current);
struct async_icount cnow;
struct async_icount cprev;
cprev = tiny->icount;
while (1) {
add_wait_queue(&tiny->wait, &wait);
set_current_state(TASK_INTERRUPTIBLE);
schedule();
remove_wait_queue(&tiny->wait, &wait);
/* see if a signal woke us up */
if (signal_pending(current))
return -ERESTARTSYS;
cnow = tiny->icount;
if (cnow.rng == cprev.rng && cnow.dsr == cprev.dsr &&
cnow.dcd == cprev.dcd && cnow.cts == cprev.cts)
return -EIO; /* no change => error */
if (((arg & TIOCM_RNG) && (cnow.rng != cprev.rng)) ||
((arg & TIOCM_DSR) && (cnow.dsr != cprev.dsr)) ||
((arg & TIOCM_CD) && (cnow.dcd != cprev.dcd)) ||
((arg & TIOCM_CTS) && (cnow.cts != cprev.cts)) ) {
return 0;
}
cprev = cnow;
}
}
return -ENOIOCTLCMD;
}
#undef tiny_ioctl
#define tiny_ioctl tiny_ioctl_tiocgicount
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg)
{
struct tiny_serial *tiny = tty->driver_data;
if (cmd == TIOCGICOUNT) {
struct async_icount cnow = tiny->icount;
struct serial_icounter_struct icount;
icount.cts = cnow.cts;
icount.dsr = cnow.dsr;
icount.rng = cnow.rng;
icount.dcd = cnow.dcd;
icount.rx = cnow.rx;
icount.tx = cnow.tx;
icount.frame = cnow.frame;
icount.overrun = cnow.overrun;
icount.parity = cnow.parity;
icount.brk = cnow.brk;
icount.buf_overrun = cnow.buf_overrun;
if (copy_to_user((void __user *)arg, &icount, sizeof(icount)))
return -EFAULT;
return 0;
}
return -ENOIOCTLCMD;
}
#undef tiny_ioctl
/* the real tiny_ioctl function. The above is done to get the small functions in the book */
static int tiny_ioctl(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case TIOCGSERIAL:
return tiny_ioctl_tiocgserial(tty, file, cmd, arg);
case TIOCMIWAIT:
return tiny_ioctl_tiocmiwait(tty, file, cmd, arg);
case TIOCGICOUNT:
return tiny_ioctl_tiocgicount(tty, file, cmd, arg);
}
return -ENOIOCTLCMD;
}
static struct tty_operations serial_ops = {
.open = tiny_open,
.close = tiny_close,
.write = tiny_write,
.write_room = tiny_write_room,
.set_termios = tiny_set_termios,
};
static struct tty_driver *tiny_tty_driver;
static int __init tiny_init(void)
{
int retval;
int i;
/* allocate the tty driver */
tiny_tty_driver = alloc_tty_driver(TINY_TTY_MINORS);
if (!tiny_tty_driver)
return -ENOMEM;
/* initialize the tty driver */
tiny_tty_driver->owner = THIS_MODULE;
tiny_tty_driver->driver_name = "tiny_tty";
tiny_tty_driver->name = "ttty";
tiny_tty_driver->devfs_name = "tts/ttty%d";
tiny_tty_driver->major = TINY_TTY_MAJOR,
tiny_tty_driver->type = TTY_DRIVER_TYPE_SERIAL,
tiny_tty_driver->subtype = SERIAL_TYPE_NORMAL,
tiny_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS,
tiny_tty_driver->init_termios = tty_std_termios;
tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
tty_set_operations(tiny_tty_driver, &serial_ops);
/* hack to make the book purty, yet still use these functions in the
* real driver. They really should be set up in the serial_ops
* structure above... */
tiny_tty_driver->read_proc = tiny_read_proc;
tiny_tty_driver->tiocmget = tiny_tiocmget;
tiny_tty_driver->tiocmset = tiny_tiocmset;
tiny_tty_driver->ioctl = tiny_ioctl;
/* register the tty driver */
retval = tty_register_driver(tiny_tty_driver);
if (retval) {
printk(KERN_ERR "failed to register tiny tty driver");
put_tty_driver(tiny_tty_driver);
return retval;
}
for (i = 0; i < TINY_TTY_MINORS; ++i)
tty_register_device(tiny_tty_driver, i, NULL);
printk(KERN_INFO DRIVER_DESC " " DRIVER_VERSION);
return retval;
}
static void __exit tiny_exit(void)
{
struct tiny_serial *tiny;
int i;
for (i = 0; i < TINY_TTY_MINORS; ++i)
tty_unregister_device(tiny_tty_driver, i);
tty_unregister_driver(tiny_tty_driver);
/* shut down all of the timers and free the memory */
for (i = 0; i < TINY_TTY_MINORS; ++i) {
tiny = tiny_table[i];
if (tiny) {
/* close the port */
while (tiny->open_count)
do_close(tiny);
/* shut down our timer and free the memory */
del_timer(tiny->timer);
kfree(tiny->timer);
kfree(tiny);
tiny_table[i] = NULL;
}
}
}
module_init(tiny_init);
module_exit(tiny_exit);
【参考文章】
Linux终端和Line discipline图解
tty驱动初步了解学习
river->init_termios = tty_std_termios;
tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
tty_set_operations(tiny_tty_driver, &serial_ops);
/* hack to make the book purty, yet still use these functions in the
/* register the tty driver */
retval = tty_register_driver(tiny_tty_driver);
if (retval) {
printk(KERN_ERR “failed to register tiny tty driver”);
put_tty_driver(tiny_tty_driver);
return retval;
}
for (i = 0; i < TINY_TTY_MINORS; ++i)
tty_register_device(tiny_tty_driver, i, NULL);
printk(KERN_INFO DRIVER_DESC " " DRIVER_VERSION);
return retval;
}
static void __exit tiny_exit(void)
{
struct tiny_serial *tiny;
int i;
for (i = 0; i < TINY_TTY_MINORS; ++i)
tty_unregister_device(tiny_tty_driver, i);
tty_unregister_driver(tiny_tty_driver);
/* shut down all of the timers and free the memory */
for (i = 0; i < TINY_TTY_MINORS; ++i) {
tiny = tiny_table[i];
if (tiny) {
/* close the port */
while (tiny->open_count)
do_close(tiny);
/* shut down our timer and free the memory */
del_timer(tiny->timer);
kfree(tiny->timer);
kfree(tiny);
tiny_table[i] = NULL;
}
}
}
module_init(tiny_init);
module_exit(tiny_exit);
【参考文章】
- [Linux终端和Line discipline图解](https://blog.csdn.net/dog250/article/details/78818612)
- [tty驱动初步了解学习](https://blog.csdn.net/zzsddre/article/details/125392085)
- LLD3