Linux 终端设备驱动

在 Linux 系统中,终端设备非常重要,没有终端设备,系统将无法向用户反馈信息,Linux 系统中包含控制台、串口和伪终端 3 类终端设备

终端设备

在 Linux 系统中,终端是一种字符型设备,它有多种类型,通常使用 tty 来简称各种类型的终端设备。tty 是 Teletype 的缩写,Teletype 是最早出现的一种终端设备,很像电传打字机,是由 Teletype 公司生产的。Linux 系统中包含如下几类终端设备。

1.串行端口终端(/dev/ttySn)
串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行端口都看作是一个字符设备。这些串行端口所对应的设备名称是/dev/ttyS0(或/dev/tts/0)、/dev/ttyS1(或/dev/tts/1)等,设备号分别是(4,0)(4,1)、等。在命令行上把标准输出重定向到端口对应的设备文件名上就可以通过该端口发送数据,例如,在命令行提示符下输入“echo test > /dev/ttyS1”会把单词“test”发送到连接在 ttyS1 端口的设备上。

2.伪终端(/dev/pty/)
伪终端(Pseudo Terminal)是成对的逻辑终端设备,并存在成对的设备文件,如/dev/ptyp3 和/dev/ttyp3,它们与实际物理设备并不直接相关。如果一个程序把 ttyp3看作是一个串行端口设备,则它对该端口的读/写操作会反映在该逻辑终端设备对应的ttyp3 上,而 ttyp3 则是另一个程序用于读写操作的逻辑设备。这样,两个程序就可以通过这种逻辑设备进行通信,使用 ttyp3 的程序会认为自己正在与一个串行端口进行通信。
以 telnet 为例,如果某人在使用 telnet 程序连接到 Linux 系统,则 telnet 程序就可能会开始连接到设备 ptyp2 上,而此时一个 getty 程序会运行在对应的 ttyp2 端口上。当 telnet 从远端获取了一个字符时,该字符就会通过 ptyp2、ttyp2 传递给 getty 程序,而 getty 程序则会通过 ttyp2、ptyp2 和 telnet 程序返回“login:”字符串信息。这样,登录程序与 telnet 程序就通过伪终端进行通信。通过使用适当的软件,可以把两个或多个伪终端设备连接到同一个物理串行端口上。

3.控制台终端(/dev/ttyn,/dev/console)
如果当前进程有控制终端(Controlling Terminal),那么/dev/tty 就是当前进程的控制终端的设备特殊文件。可以使用命令“ps –ax”来查看进程与哪个控制终端相连,使用命令“tty”可以查看它具体对应哪个实际终端设备。/dev/tty 有些类似于到实际所使用终端设备的一个连接。

在 UNIX 系统中,计算机显示器通常被称为控制台终端(Console)。它仿真了类型为 Linux 的一种终端(TERM = Linux) 并且有一些设备特殊文件与之相关联:tty0、tty1、tty2 等。当用户在控制台上登录时,使用的是 tty1。按[Alt+F1]~[Alt+F6]组合键时,我们就可以切换到 tty2、tty3 等。tty1~tty6 等称为虚拟终端,而 tty0 则是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上。因此不管当前正在使用哪个虚拟终端,系统信息都会发送到控制台终端上。用户可以登录到不同的虚拟终端上去,因而可以让系统同时有几个不同的会话期存在。只有系统或超级用户 root可以向/dev/tty0 进行写操作。

在 Linux 系统中,可以在系统启动命令行里指定当前的输出终端,格式如下:

console=device, options
device 指代的是终端设备,可以是 tty0(前台的虚拟终端)、ttyX(第 X 个虚拟终端)、ttySX(第 X 个串口)、lp0(第一个并口)等。
options 指代对 device 进行的设置,它取决于具体的设备驱动。对于串口设备,参数用来定义为如下。

波特率、校验位、位数,格式为 BBBBPN,其中 BBBB 表示波特率,P 表示校验(n/o/e) 表示位数,默认 options 是 9600n8。

用户可以在内核命令行中同时设定多个终端,这样输出将会在所有的终端上显示,而当用户调用 open()打开/dev/console 时,打开的将是设定的最后一个终端。
例如:

console=ttyS1, 9600 console=tty0
定义了两个终端,而调用 open()打开/dev/console 时,将使用虚拟终端 tty0。但是内核消息会在 tty0 VGA 虚拟终端和串口 ttyS1 上同时显示

通过查看/proc/tty/drivers 文件可以获知什么类型的 tty 设备存在以及什么驱动被加载到内核,这个文件包括一个当前存在的不同 tty 驱动的列表,包括驱动名、默认的节点名、驱动的主编号、这个驱动使用的次编号范围以及 tty 驱动的类型。例如,
下面所示为一个/proc/tty/drivers 文件的例子。

/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
rfcomm               /dev/rfcomm   216 0-255 serial
ttyprintk            /dev/ttyprintk   5       3 console
serial               /dev/ttyS       4 64-111 serial
pty_slave            /dev/pts      136 0-1048575 pty:slave
pty_master           /dev/ptm      128 0-1048575 pty:master
unknown              /dev/tty        4 1-63 console
终端设备驱动结构

Linux 内核中 tty 的层次结构如下图所示,包含 tty 核心、tty 线路规程和 tty 驱动,tty 线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据,这种格式化常常采用一个协议转换的形式,例如 PPP 和 Bluetooth。tty 设备发送数据的流程为:tty 核心从一个用户获取将要发送给一个 tty 设备的数据,tty 核心将数据传递给 tty 线路规程驱动,接着数据被传递到 tty驱动,tty 驱动将数据转换为可以发送给硬件的格式。

接收数据的流程为:从 tty 硬件接收到的数据向上交给 tty 驱动,进入 tty 线路规程驱动,再进入 tty 核心,在这里它被一个用户获取。尽管大多数时候 tty 核心和 tty 之间的数据传输会经历 tty 线路规程的转换,但是 tty 驱动与 tty 核心之间也可以直接传输数据。

下图显示了与 tty 相关的主要源文件及数据的流向。tty_io.c 定义了 tty 设备通用的 file_ operations 结构体并实现了接口函数tty_register_driver()用于注册 tty 设备,它会利用 fs/char_dev.c 提供的接口函数注册字符设备,与具体设备对应的 tty 驱动将实现 tty_driver 结构体中的成员函数。同时 tty_io.c 也提供了tty_register_ldisc()接口函数用于注册线路规程,n_tty.c 文件则实现了 tty_disc 结构体中的成员。


从上图可以看出,特定 tty 设备驱动的主体工作是填充 tty_driver 结构体中的成员,实现其中的成员函数,tty_driver 结构体的定义如代所示。

struct tty_driver {
	int	magic;		/* magic number for this structure */
	struct cdev cdev;       //对应的字符设备 cdev
	struct module	*owner; /*这个驱动的模块拥有者 */
	const char	*driver_name;
	const char	*name;   /* 设备名 */
	int	name_base;	/* offset of printed name */
	int	major;		/* 主设备号 */
	int	minor_start;	/* 开始次设备号 */
	int	minor_num;	/* 设备数量 */
	int	num;		/* 被分配的设备数量 */
	short	type;		/* tty 驱动的类型 */
	short	subtype;	/* tty 驱动的子类型 */
	struct ktermios init_termios; /* 初始线路设置 */
	int	flags;		/* tty 驱动标志 */
	int	refcount;	/*引用计数(针对可加载的 tty 驱动) */
	struct proc_dir_entry *proc_entry; /* /proc 文件系统入口 */
	struct tty_driver *other; /* 仅对 PTY 驱动有意义 */

	/*
	 * Pointer to the tty data structures
	 */
	struct tty_struct **ttys;
	struct ktermios **termios;
	struct ktermios **termios_locked;
	void *driver_state;	/* only used for the PTY driver */
	
	/*
	 * 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);
	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;//tty驱动链表
};
tty_driver 结 构 体 中 的 magic 表 示 给 这 个 结 构 体 的 “ 幻 数 ” 设 为TTY_DRIVER_MAGIC,在 alloc_tty_driver()函数中被初始化。 TTY_DRIVER_MAGIC,在 alloc_tty_driver()函数中被初始化。

name 与 driver_name 的不同在于后者表示驱动的名字,用在 /proc/tty 和 sysfs中,而前者表示驱动的设备节点名。

type 与 subtype 描述 tty 驱动的类型和子类型,subtype 的值依赖于 type,type 成员的可能值为 TTY_DRIVER_TYPE_SYSTEM(由 tty 子系统内部使用,subtype 应当设为SYSTEM_TYPE_TTY、SYSTEM_TYEP_CONSOLE、SYSTEM_TYPE_SYSCONS 或
SYSTEM_TYPE_SYSPTMX , 这 个 类 型 不 应 当 被 任 何 常 规 tty 驱 动 使 用 )、TTY_DRIVER_TYPE_CONSOLE ( 仅 被 控 制 台 驱 动 使 用 ) 、TTY_DRIVER_TYPE_SERIAL ( 被 任 何 串 行 类 型 驱 动 使 用 , subtype 应 当 设 为SERIAL_TYPE_NORMAL
或SERIAL_TYPE_CALLOUT)
 、TTY_DRIVER_TYPE_PTY(被伪控制台接口 pty 使用,此时 subtype 需要被设置为
PTY_TYPE_MASTER 或 PTY_TYPE_SLAVE)。

init_termios 为初始线路设置,为一个 termios 结构体,这个成员被用来提供一个线路设置集合。

termios 用于保存当前的线路设置,这些线路设置控制当前波特率、数据大小、数据流控设置等,这个结构体包含 tcflag_t c_iflag(输入模式标志)、tcflag_t c_oflag(输出模式标志)、tcflag_t c_cflag(控制模式标志)、tcflag_t c_lflag(本地模式标志)、cc_t c_line(线路规程类型)、cc_t c_cc[NCCS](一个控制字符数组)等成员。

驱动会使用一个标准的数值集初始化这个成员,这个数值集来源于 tty_std_termios变量,tty_std_termos 在 tty 核心中的定义如代码所示。(这个在内核中没有找到,以后分析

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 /* 控制字符,用来修改终端的特殊字符映射 */
};
ty_driver 结构体中的 major、minor_start、minor_num 表示主设备号、次设备号及可能的次设备数,name 表示设备名(如 ttyS)
,函数指针实际和 tty_operations 结构体等同,它们通常需在特定设备 tty 驱动模块初始化函数中被赋值。
put_char()为单字节写函数,当单个字节被写入设备时这个函数被 tty 核心调用,如果一个 tty 驱动没有定义这个函数,将使用 count 参数为 1 的 write()函数。
flush_chars()与 wait_until_sent()函数都用于刷新数据到硬件。
write_room()指示有多少缓冲区空闲,chars_in_buffer()指示缓冲区的长度。
当 ioctl(2)在设备节点上被调用时,ioctl()函数将被 tty 核心调用。
当设备的 termios 设置被改变时,set_termios()函数将被 tty 核心调用。

throttle()、unthrottle()、stop()和 start()用于控制 tty 核心的输入缓存。当 tty 核心的输入缓冲满时,throttle()函数将被调用,tty 驱动试图通知设备不应当发送字符给它。当 tty 核心的输入缓冲已被清空时,unthrottle()函数将被调用暗示设备可以接收数据。stop()和 start()函数非常像 throttle()和 unthrottle()函数,但它们表示 tty驱动应当停止发送数据给设备以及恢复发送数据

当 tty 驱动挂起 tty 设备时,hangup()函数被调用,在此函数中进行相关的硬件操作。
当 tty 驱动要在 RS-232 端口上打开或关闭线路的 BREAK 状态时,break_ctl()线路中断控制函数被调用。如果 state 状态设为−1,BREAK 状态打开,如果状态设为 0,BREAK 状态关闭。如果这个函数由 tty 驱动实现,而 tty 核心将处理 TCSBRK、TCSBRKP、TIOCSBRK 和 TIOCCBRK 这些 ioctl 命令。

flush_buffer()函数用于刷新缓冲区并丢弃任何剩下的数据。
set_ldisc()函数用于设置线路规程, tty 核心改变 tty 驱动的线路规程时这个函数被调用,这个函数通常不需要被驱动定义。
send_xchar()为 X-类型字符发送函数,这个函数用来发送一个高优先级 XON 或者XOFF 字符给 tty 设备,要被发送的字符在第二个参数 ch 中指定。
read_proc()和 write_proc()为/proc 文件系统的读和写函数。
tiocmget()函数用于获得 tty 设备的线路设置,对应的 tiocmset()用于设置 tty 设备的线路设置,参数 set 和 clear 包含了要设置或者清除的线路设置。

Linux 内核提供了一组函数用于操作 tty_driver 结构体及 tty 设备,如下所示。

1.分配 tty 驱动

struct tty_driver *alloc_tty_driver(int lines);
这个函数返回 tty_driver 指针,其 参数为要分配的设备数量,line 会被赋值给tty_driver 的 num 成员,例如:
xxx_tty_driver = alloc_tty_driver(XXX_TTY_MINORS);
if (!xxx_tty_driver) //分配失败
  return -ENOMEM;
2.注册 tty 驱动
int tty_register_driver(struct tty_driver *driver);
注册 tty 驱动成功时返回 0,参数为由 alloc_tty_driver ()分配的 tty_driver 结构体指针,例如:
retval = tty_register_driver(xxx_tty_driver);
if (retval) //注册失败
{
   printk(KERN_ERR "failed to register tiny tty driver");
   put_tty_driver(xxx_tty_driver);
   return retval;
}
3.注销 tty 驱动
int tty_unregister_driver(struct tty_driver *driver);
这个函数与 tty_register_driver ()对应, 驱动最终会调用上述函数注销 tty_driver。
4.注册 tty 设备

void tty_register_device(struct tty_driver *driver, unsigned index,struct device *device);
仅有 tty_driver 是不够的,驱动必须依附于设备,tty_register_device()函数用于注册关联于 tty_driver 的设备, index 为设备的索引(范围是 0 ~ driver->num),例如:
for (i = 0; i < XXX_TTY_MINORS; ++i)
   tty_register_device(xxx_tty_driver, i, NULL);
5.注销 tty 设备
void tty_unregister_device(struct tty_driver *driver, unsigned index);
上述函数与 tty_register_device()对应,用于注销 tty 设备,其使用方法如下:
for (i = 0; i < XXX_TTY_MINORS; ++i)
   tty_unregister_device(xxx_tty_driver, i);
6.设置 tty 驱动操作

void tty_set_operations(struct tty_driver *driver,struct tty_operations *op);
上述函数会将 tty_operations 结构体中的函数指针复制到 tty_driver 对应的函数指针,在具体的 tty 驱动中,通常会定义一个设备特定的 tty_operations,tty_operations的定义如代码如下所示。tty_operations 中的成员函数与 tty_driver 中的同名成员函数意义完全一致,因此,这里不再赘述。

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);
	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);
};
终端设备驱动都围绕 tty_driver 结构体而展开,一般而言,终端设备驱动应包含如下组成。
• 终端设备驱动模块加载函数和卸载函数:完成注册和注销 tty_driver,初始化和释放终端设备对应的 tty_driver 结构体成员及硬件资源。
• 实现 tty_operations 结构体中的一系列成员函数:主要是实现 open()、close()、write()、tiocmget()、tiocmset()等函数。

终端设备驱动的初始化与释放

模块加载与卸载函数
tty 驱动的模块加载函数中通常需要分配、初始化 tty_driver 结构体并申请必要的硬件资源,如代码如下所示。tty 驱动的模块卸载函数完成与模块加载函数相反的工作。

/* tty 驱动的模块加载函数 */
static int _ _init xxx_init(void)
{
  ...
  /* 分配 tty_driver 结构体 */
  xxx_tty_driver = alloc_tty_driver(XXX_PORTS);
  /* 初始化 tty_driver 结构体 */
  xxx_tty_driver->owner = THIS_MODULE;
  xxx_tty_driver->devfs_name = "tts/";
  xxx_tty_driver->name = "ttyS";
  xxx_tty_driver->major = TTY_MAJOR;
  xxx_tty_driver->minor_start = 64;
  xxx_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;//串口设备类型
  xxx_tty_driver->subtype = SERIAL_TYPE_NORMAL;
  xxx_tty_driver->init_termios = tty_std_termios;
  xxx_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
  xxx_tty_driver->flags = TTY_DRIVER_REAL_RAW;
  tty_set_operations(xxx_tty_driver, &xxx_ops);//设置tty_driver的操作

  ret = tty_register_driver(xxx_tty_driver);//注册tty_driver
  if (ret)
  {
    printk(KERN_ERR "Couldn't register xxx serial driver\n");
    put_tty_driver(xxx_tty_driver);
   return ret;
  }
  ...
  ret = request_irq(...); /* 硬件资源申请 */
  ...
}
打开与关闭函数
当用户对 tty 驱动所分配的设备节点进行 open()系统调用时,tty_driver 中的 open()成员函数将被 tty 核心调用。 tty 驱动必须设置 open()成员,否则,-ENODEV 将被返回给调用 open()的用户。
open()成员函数的第一个参数为一个指向分配给这个设备的 tty_struct 结构体的指针,第二个参数为文件指针。tty_struct 结构体被 tty 核心用来保存当前 tty 端口的状态,它的大多数成员只被 tty核心使用。tty_struct 中的几个重要成员如下。
struct tty_struct {
	int	magic;
	struct tty_driver *driver;
	int index;
	struct tty_ldisc ldisc;
	struct mutex termios_mutex;
	struct ktermios *termios, *termios_locked;
	char name[64];
	struct pid *pgrp;
	struct pid *session;
	unsigned long flags;
	int count;
	struct winsize winsize;
	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;
	wait_queue_head_t read_wait;
	struct work_struct hangup_work;
	void *disc_data;
	void *driver_data;
	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;
	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;
};
1. flags 标示 tty 设备的当前状态,包括 TTY_THROTTLED、TTY_IO_ERROR、TTY_OTHER_CLOSED 、 TTY_EXCLUSIVE 、 TTY_DEBUG 、TTY_DO_WRITE_WAKEUP、TTY_PUSH、TTY_CLOSING、TTY_DONT_FLIP、TTY_HW_COOK_OUT 、 TTY_HW_COOK_IN 、 TTY_ PTY_LOCK 、TTY_NO_WRITE_SPLIT 等。
2. ldisc 为给 tty 设备使用的线路规程。
3. write_waitread_wait 为 tty 写/读函数的等待队列,tty 驱动应当在合适的时机唤醒对应的等待队列。
4. termios 为指向 tty 设备的当前 termios 设置的指针。
5.stopped:1 指示是否停止 tty 设备,tty 驱动可以设置这个值;hw_stopped:1指示是否 tty 设备已经被停止,tty 驱动可以设置这个值;flow_stopped:1 指示是否 tty 设备数据流停止。
6.driver_data、disc_data 为数据指针,是用于存储 tty 驱动( tty_driver)和线路规程( tty_ldisc)的“私有”数据。驱动中可以定义一个设备相关的结构体,并在 open()函数中将其赋值给 tty_struct的 driver_data 成员,如代码清单如下所示。
/* 设备“私有”数据结构体 */
struct xxx_tty
{
   struct tty_struct *tty; /* tty_struct 指针 */
   int open_count; /* 打开次数 */
   struct semaphore sem; /* 结构体锁定信号量 */
   int xmit_buf; /* 传输缓冲区 */
   ...
}
/* 打开函数 */
static int xxx_open(struct tty_struct *tty, struct file *file)
{
   struct xxx_tty *xxx;
   /* 分配 xxx_tty */
   xxx = kmalloc(sizeof(*xxx), GFP_KERNEL);
   if (!xxx)
     return - ENOMEM;
   /* 初始化 xxx_tty 中的成员 */
   init_MUTEX(&xxx->sem);
   xxx->open_count = 0;
   ...
   /* 使 tty_struct 中的 driver_data 指向 xxx_tty */
   tty->driver_data = xxx;
   xxx->tty = tty;
   ...
   return 0;
}
数据发送和接收

如上图所示为终端设备数据发送和接收过程中的数据流以及函数调用关系。用户在有数据发送给终端设备时,通过“write()系统调用—tty 核心—线路规程”的层层调用,最终调用 tty_driver 结构体中的 write()函数完成发送。因为速度和 tty 硬件缓冲区容量的原因,不是所有的写程序要求的字符都可以在调用写函数时被发送,因此写函数应当返回能够发送给硬件的字节数以便用户程序检查是否所有的数据被真正写入。如果在 wirte()调用期间发生任何错误,一个负的错误码应当被返回。

tty_driver 的 write()函数接受 3 个参数:tty_struct、发送数据指针及要发送的字节数,一般首先会通过 tty_struct 的 driver_data 成员得到设备私有信息结构体,然后依次进行必要的硬件操作开始发送,
如代码清单如下所示为 tty_driver 的 write()函数范例。

static int xxx_write(struct tty_struct *tty, const unsigned char *buf,int count)
{
   /* 获得 tty 设备私有数据 */
   struct xxx_tty *xxx = (struct xxx_tty*)tty->driver_data;
   ...
   /* 开始发送 */
   while (1)
   {
     local_irq_save(flags);
     c = min_t(int, count, min(SERIAL_XMIT_SIZE - xxx->xmit_cnt - 1,SERIAL_XMIT_SIZE - xxx->xmit_head));
     if (c <= 0)
     {
       local_irq_restore(flags);
       break;
     }
     //复制到发送缓冲区
     memcpy(xxx->xmit_buf + xxx->xmit_head, buf, c);
     xxx->xmit_head = (xxx->xmit_head + c) &(SERIAL_XMIT_SIZE - 1);
     xxx->xmit_cnt += c;
     local_irq_restore(flags);
     buf += c;
     count -= c;
     total += c;
   }

   if (xxx->xmit_cnt && !tty->stopped && !tty->hw_stopped)
   {
     start_xmit(xxx);//开始发送
   }
   return total; //返回发送的字节数
}
当 tty 子系统自己需要发送数据到 tty 设备时,如果没有实现 put_char()函数,write()函数将被调用,此时传入的 count 参数为 1,通过对下面代码的分析即可获知。
int tty_register_driver(struct tty_driver *driver)
{
  ...
  if (!driver->put_char)//没有定义 put_char()函数
    driver->put_char = tty_default_put_char;
  ...
}
static void tty_default_put_char(struct tty_struct *tty, unsigned char ch)
{
  tty->driver->write(tty, &ch, 1);//调用 tty_driver.write()函数
}
ty_driver 结构体中没有提供 read()函数。因为发送是用户主动的,而 接收即用户调用 read()则是读一片缓冲区中已放好的数据。tty 核心在一个称为 struct tty_flip_buffer的结构体中缓冲数据直到它被用户请求。因为 tty 核心提供了缓冲逻辑,因此每个 tty驱动并非一定要实现它自身的缓冲逻辑。tty 驱动不必过于关心 tty_flip_buffer 结构体的细节,如果其 count 字段大于或等于TTY_ FLIP BUF_SIZE , 这 个 flip 缓 冲 区 就 需 要 被 刷 新 到 用 户 , 刷 新 通 过 对tty_flip_buffer_push()函数的调用来完成,如下代码所示为 tty_flip_buffer_push()的使用范例。
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);
从 tty 驱动接收到字符将被 tty_insert_flip_char()函数插入到 flip 缓冲区。该函数的第 1 个参数是数据应当保存入的 tty_struct 结构体,第 2 个参数是要保存的字符,第3 个参数是应当为这个字符设置的标志,如果字符是一个接收到的常规字符,则设为 TTY_NORMAL,如果是一个特殊类型的指示错误的字符,依据具体的错误类型,应当设为 TTY_BREAK、TTY_PARITY 或 TTY_OVERRUN。


tty 线路设置

线路设置用户空间接口
用户可用如下两种方式改变 tty 设备的线路设置或者获取当前线路设置。
1.调用用户空间的 termios 库函数
用户空间的应用程序需引用 termios.h 头文件,该头文件包含了终端设备的 I/O 接口,实际是由 POSIX 定义的标准方法。对终端设备操作模式的描述由 termios 结构体完成,从之前代码可以看出,这个结构体包含 c_iflag、c_oflag、c_cflag、c_lflag和 c_cc[]几个成员。

termios 的 c_cflag 主要包含如下位域信息:CSIZE(字长)、CSTOPB(两个停止位), PARENB(奇偶校验位使能)、PARODD (奇校验位),当 PARENB 被使能时CREAD(字符接收使能,如果没有置位,仍然从端口接收字符,但这些字符都、要被丢弃) CRTSCTS (如果被置位,CTS 状态改变时将发送报告)、、CLOCAL (如果没有置位,调制解调器状态改变时将发送报告)。

termios 的 c_iflag 主要包含如下位域信息:INPCK(使能帧和奇偶校验错误检查)、BRKINT(break 将清除终端输入/输出队列,向该终端上前台的程序发出 SIGINT 信号) PARMRK (奇偶校验和帧错误被标记,在 INPCK 被设置且 IGNPAR 未被设置的情况下才有意义)、IGNPAR (忽略奇偶校验和帧错误)、IGNBRK (忽略 break)。

通过 tcgetattr()、tcsetattr()函数即可完成对终端设备的操作模式的设置和获取,这
两个函数的原型如下:

int tcgetattr (int fd, struct termios *termios_p);
int tcsetattr (int fd, int optional_actions, struct termios *termios_p);


UART 设备驱动

尽管一个特定的 UART 设备驱动完全可以遵循上面的方法来设计,即定义 tty_driver 并实现其中的成员函数,但是 Linux 内核已经在文件 serial_core.c 中实现了 UART 设备的通用 tty 驱动层(称为串口核心层),这样,UART 驱动的主要任务演变成实现 serial-core.c 中定义的一组 uart_xxx 接口而非 tty_xxx 接口,如下图所示。

serial_core.c 串口核心层完全可以被当作上面所说的tty 设备驱动的实例,它实现了 UART 设备的 tty 驱动。

串口核心层为串口设备驱动提供了如下 3 个结构体。

1.uart_driver
uart_driver 包含串口设备的驱动名、设备名、设备号等信息,它封装了 tty_driver,使得底层的 UART 驱动无须关心 tty_driver,其定义如代码清单所示。

struct uart_driver {
	struct module		*owner;
	const char		*driver_name;//驱动名
	const char		*dev_name;//设备名
	int			 major;//主设备号
	int			 minor;//次设备号
	int			 nr;
	struct console		*cons;

	/*
	 * these are private; the low level driver should not
	 * touch these; they should be initialised to NULL
	 *//* 私有的,底层驱动不应该访问这些成员,应该被初始化为 NULL */
	struct uart_state	*state;
	struct tty_driver	*tty_driver;
};
一个 tty 驱动必须注册/注销 tty_driver,而一个 UART 驱动则演变为注册/注销uart_driver,使用如下接口:
int uart_register_driver(struct uart_driver *drv);
void uart_unregister_driver(struct uart_driver *drv);
实 际 上 , uart_register_driver() 和 uart_unregister_driver() 中 分 别 包 含 了tty_register_driver()和 tty_unregister_driver()的操作,如代码清单所示。
int uart_register_driver(struct uart_driver *drv)
{
   struct tty_driver *normal = NULL;
   int i, retval;
   ...
   /* 分配 tty_driver */
   normal = alloc_tty_driver(drv->nr);
   if (!normal)
     goto out;
   drv->tty_driver = normal;
   /* 初始化 tty_driver */
   normal->owner = drv->owner;
   normal->driver_name = drv->driver_name;
   normal->devfs_name = drv->devfs_name;
   normal->name = drv->dev_name;
   normal->major = drv->major;
   normal->minor_start = drv->minor;
   normal->type = TTY_DRIVER_TYPE_SERIAL;
   normal->subtype = SERIAL_TYPE_NORMAL;
   normal->init_termios = tty_std_termios;
   normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
   normal->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS;
   normal->driver_state = drv;
   tty_set_operations(normal, &uart_ops);
   ...
   /* 注册 tty 驱动 */
   retval = tty_register_driver(normal);
   out:
     if (retval < 0) {
       put_tty_driver(normal);
       kfree(drv->state);
    }
   return retval;
}

void uart_unregister_driver(struct uart_driver *drv)
{
  struct tty_driver *p = drv->tty_driver;
  tty_unregister_driver(p); /* 注销 tty 驱动 */
  put_tty_driver(p);
  kfree(drv->state);
  drv->tty_driver = NULL;
}
2.uart_port
uart_port 用于描述一个 UART 端口(直接对应于一个串口)的 I/O 端口或 I/O 内存地址、FIFO 大小、端口类型等信息,其定义如代码清单如下所示。
struct uart_port {
	spinlock_t		lock;			/* 端口锁 */
	unsigned int		iobase;			/* I/O 端口基地址 */
	unsigned char __iomem	*membase;		/* I/O 内存基地址 */
	unsigned int		irq;			/* 中断号 */
	unsigned int		uartclk;		/* UART 时钟 */
	unsigned int		fifosize;		/* 传输 fifo 大小 */
	unsigned char		x_char;			/* xon/xoff 字符 */
	unsigned char		regshift;		/* 寄存器位移 */
	unsigned char		iotype;			/* I/O 存取类型 */
	unsigned char		unused1;

#define UPIO_PORT		(0)
#define UPIO_HUB6		(1)
#define UPIO_MEM		(2)
#define UPIO_MEM32		(3)
#define UPIO_AU			(4)			/* Au1x00 type IO */
#define UPIO_TSI		(5)			/* Tsi108/109 type IO */
#define UPIO_DWAPB		(6)			/* DesignWare APB UART */
#define UPIO_RM9000		(7)			/* RM9000 type IO */

	unsigned int		read_status_mask;	/* driver specific */
	unsigned int		ignore_status_mask;	/* driver specific */
	struct uart_info	*info;			/* 指向 parent 信息 */
	struct uart_icount	icount;			/* statistics */

	struct console		*cons;			/* struct console, if any */
#ifdef CONFIG_SERIAL_CORE_CONSOLE
	unsigned long		sysrq;			/* sysrq timeout */
#endif

	upf_t			flags;

#define UPF_FOURPORT		((__force upf_t) (1 << 1))
#define UPF_SAK			((__force upf_t) (1 << 2))
#define UPF_SPD_MASK		((__force upf_t) (0x1030))
#define UPF_SPD_HI		((__force upf_t) (0x0010))
#define UPF_SPD_VHI		((__force upf_t) (0x0020))
#define UPF_SPD_CUST		((__force upf_t) (0x0030))
#define UPF_SPD_SHI		((__force upf_t) (0x1000))
#define UPF_SPD_WARP		((__force upf_t) (0x1010))
#define UPF_SKIP_TEST		((__force upf_t) (1 << 6))
#define UPF_AUTO_IRQ		((__force upf_t) (1 << 7))
#define UPF_HARDPPS_CD		((__force upf_t) (1 << 11))
#define UPF_LOW_LATENCY		((__force upf_t) (1 << 13))
#define UPF_BUGGY_UART		((__force upf_t) (1 << 14))
#define UPF_MAGIC_MULTIPLIER	((__force upf_t) (1 << 16))
#define UPF_CONS_FLOW		((__force upf_t) (1 << 23))
#define UPF_SHARE_IRQ		((__force upf_t) (1 << 24))
#define UPF_BOOT_AUTOCONF	((__force upf_t) (1 << 28))
#define UPF_FIXED_PORT		((__force upf_t) (1 << 29))
#define UPF_DEAD		((__force upf_t) (1 << 30))
#define UPF_IOREMAP		((__force upf_t) (1 << 31))

#define UPF_CHANGE_MASK		((__force upf_t) (0x17fff))
#define UPF_USR_MASK		((__force upf_t) (UPF_SPD_MASK|UPF_LOW_LATENCY))

	unsigned int		mctrl;			/* 目前 modem 控制设置 */
	unsigned int		timeout;		/* 基于字符的超时 */
	unsigned int		type;			/* 端口类型 */
	const struct uart_ops	*ops;                   /* UART 操作集 */
	unsigned int		custom_divisor;
	unsigned int		line;			/* 端口索引 */
	unsigned long		mapbase;		/* ioremap 后基地址 */
	struct device		*dev;			/* parent 设备 */
	unsigned char		hub6;			/* this should be in the 8250 driver */
	unsigned char		unused[3];
	void			*private_data;		/* generic platform data pointer */
};
串口核心层提供如下函数来添加一个端口:
int uart_add_one_port(struct uart_driver *drv, struct uart_port *port);
对上述函数的调用应该发生在 uart_register_driver()之后,uart_add_one_port()的一个最重要作用是封装了 tty_register_device()。
uart_add_one_port() 的 “ 反 函 数 ” 是 uart_remove_one_port() , 其 中 会 调 用 tty_unregister_device(),原型为:
int uart_remove_one_port(struct uart_driver *drv, struct uart_port *port);
驱动中虽然不需要处理 uart_port 的 uart_info 成员,但是在发送时,从用户来的数据被保存在 xmit(被定义为 circ_buf,即环形缓冲区)中,因此 UART 驱动在发送数据时( 一般在发送中断处理函数中),需要从这个 circ_buf 获取上层传递下来的字符。

3.uart_ops
uart_ops 定义了对 UART 的一系列操作,包括发送、接收及线路设置等,如果说tty_driver 中的 tty_operations 对于串口还较为抽象,那么 uart_ops 则直接面向了串口的 UART,其定义如代码清单如下所示。Linux 驱动的这种层次非常类似于面向对象编程中基类、派生类的关系,派生类针对特定的事物会更加具体,而基类则处于更高的抽象层次上。

struct uart_ops {
	unsigned int	(*tx_empty)(struct uart_port *);
	void		(*set_mctrl)(struct uart_port *, unsigned int mctrl);
	unsigned int	(*get_mctrl)(struct uart_port *);
	void		(*stop_tx)(struct uart_port *);//停止发送
	void		(*start_tx)(struct uart_port *);//开始发送
	void		(*send_xchar)(struct uart_port *, char ch);//发送 xchar
	void		(*stop_rx)(struct uart_port *);//停止接收
	void		(*enable_ms)(struct uart_port *);
	void		(*break_ctl)(struct uart_port *, int ctl);
	int		(*startup)(struct uart_port *);
	void		(*shutdown)(struct uart_port *);
	void		(*set_termios)(struct uart_port *, struct ktermios *new,
				       struct ktermios *old);//设置 termios
	void		(*pm)(struct uart_port *, unsigned int state,
			      unsigned int oldstate);
	int		(*set_wake)(struct uart_port *, unsigned int state);

	/*
	 * Return a string describing the type of the port
	 */
	const char *(*type)(struct uart_port *);/* 返回一个描述端口类型的字符串 */

	/*
	 * Release IO and memory resources used by the port.
	 * This includes iounmap if necessary.
	 *//* 释放端口使用的 I/O 和内存资源,必要的情况下,应该进行 iounmap 操作 */
	void		(*release_port)(struct uart_port *);

	/*
	 * Request IO and memory resources used by the port.
	 * This includes iomapping the port if necessary.
	 *//* 申请端口使用的 I/O 和内存资源 */
	int		(*request_port)(struct uart_port *);
	void		(*config_port)(struct uart_port *, int);
	int		(*verify_port)(struct uart_port *, struct serial_struct *);
	int		(*ioctl)(struct uart_port *, unsigned int, unsigned long);
};
serial_core.c 中定义了 tty_operations 的实例,包含 uart_open()、uart_close()、uart_write()、uart_send_xchar()等成员函数(如下面代码所示),这些函数会借助uart_ops 结构体中的成员函数来完成具体的操作,下面代码所示 tty_operations的 uart_send_xchar()成员函数利用 uart_ops 中 start_tx()、send_xchar()成员函数的例子。
static const struct tty_operations uart_ops = {
	.open		= uart_open,//串口打开
	.close		= uart_close,//串口关闭
	.write		= uart_write,//串口发送
	.put_char	= uart_put_char,
	.flush_chars	= uart_flush_chars,
	.write_room	= uart_write_room,
	.chars_in_buffer= uart_chars_in_buffer,
	.flush_buffer	= uart_flush_buffer,
	.ioctl		= uart_ioctl,
	.throttle	= uart_throttle,
	.unthrottle	= uart_unthrottle,
	.send_xchar	= uart_send_xchar,
	.set_termios	= uart_set_termios,
	.stop		= uart_stop,
	.start		= uart_start,
	.hangup		= uart_hangup,
	.break_ctl	= uart_break_ctl,
	.wait_until_sent= uart_wait_until_sent,
#ifdef CONFIG_PROC_FS
	.read_proc	= uart_read_proc,//proc 入口读函数
#endif
	.tiocmget	= uart_tiocmget,
	.tiocmset	= uart_tiocmset,
};
串口核心层的 tty_operations 与 uart_ops 关系

static void uart_send_xchar(struct tty_struct *tty, char ch)
{
   struct uart_state *state = tty->driver_data;
   struct uart_port *port = state->port;
   unsigned long flags;
   //如果 uart_ops 中实现了 send_xchar 成员函数
   if (port->ops->send_xchar)
     port->ops->send_xchar(port, ch);
   else //uart_ops 中未实现 send_xchar 成员函数
   {
     port->x_char = ch; //xchar 赋值
     if (ch)
     {
       spin_lock_irqsave(&port->lock, flags);
       port->ops->start_tx(port); //发送 xchar
       spin_unlock_irqrestore(&port->lock, flags);
     }
    }
}
在使用串口核心层这个通用串口 tty 驱动层的接口后,一个串口驱动要完成的主要工作如下。
1.定义 uart_driver、uart_ops、uart_port 等结构体的实例并在适当的地方根据具体硬件和驱动的情况初始化它们,当然具体设备 xxx 的驱动可以将这些结构套在新定义的 xxx_uart_ driver、xxx_uart_ops、xxx_uart_port 之内。
2.在 模 块 初 始 化 时 调 用 uart_register_driver() 和 uart_add_one_port() 以 注 册UART 驱 动 并 添 加 端 口 , 在 模 块 卸 载 时 调 用 uart_unregister_driver() 和uart_remove_one_port()以注销 UART 驱动并移除端口。
3.根据具体硬件的 datasheet 实现 uart_ops 中的成员函数,这些函数的实现成为UART 驱动的主体工作。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值