come from:http://blog.sina.com.cn/s/blog_569a151b0100nu8w.html
TTY驱动
让我们开始看看tty驱动的核心结构体和注册函数。有三个结构体非常重要:
- 定义于include/linux/tty.h中的tty_struct结构体。此结构体包含了和打开的tty相关的所有的状态信息。其结构体成员众多,下面列出了一些重要的成员:
struct tty_struct {
int magic;
struct tty_driver *driver;
struct tty_ldisc ldisc;
struct tty_flip_buffer flip;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
};
- tty_struct结构体中的tty_flip_buffer结构体。这是数据收集和处理机制的中枢:
struct tty_flip_buffer {
struct semaphore pty_sem;
char *char_buf_ptr;
unsigned char char_buf[2*TTY_FLIPBUF_SIZE];
};
底层的串行驱动将flip缓冲区的一半用于数据收集,线路规程则使用另一半来进行数据处理。数据处理伴随着串行驱动和线路规程所使用的缓冲区指针的移动而持续进行。从drivers/char/tty_io.c文件的flush_to_ldisc()函数中可看到flip的确切行为。
在最近的内核中,tty_flip_buffer结构体有些改动,该结构体目前由缓冲区头部(tty_bufhead)和缓冲区链表(tty_buffer)组成:
struct tty_bufhead {
struct semaphore pty_sem;
struct tty_buffer *head, tail, free;
};
struct tty_buffer {
struct tty_buffer *next;
char *char_buf_ptr;
unsigned long data[0];
};
- 定义于include/linux/tty_driver.h文件中的tty_driver结构体。它规定了tty驱动和高层的编程接口:
struct tty_driver {
int magic;
int major;
int minor_start;
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);
};
像UART驱动一样,tty驱动也需要完成两个步骤以向内核注册自身:
1. |
调用tty_register_driver(struct tty_driver *tty_d)向tty核心注册自身。 |
2. |
调用
tty_register_device(struct tty_driver *tty_d,
unsigned device_index,
struct device *device)
注册它支持的每个单独的tty。 |
本章不会给出tty驱动的实例,在Linux内核中有一些通用的tty驱动:
- 第16章讨论的蓝牙模拟串口,就是用tty驱动的形式实现的。此驱动(drivers/net/bluetooth/rfcomm/tty.c)在初始化阶段调用tty_register_driver(),在处理每个到来的蓝牙连接时调用tty_register_device()。
- 在Linux桌面上,为了使用系统控制台,如果你工作在字符模式下,将需要虚拟终端(virtual terminals,VTs)服务,如果你处在图形模式下,将需要伪终端(pseudo terminals,PTYs)服务。虚拟终端和伪终端都是用tty驱动实现的,分别位于drivers/char/vt.c和drivers/char/pty.c。
- 传统的UART使用的tty驱动位于drivers/serial/serial_core.c。
- USB到串口转换器的tty驱动位于drivers/usb/serial/usb-serial.c。
线路规程
线路规程提供了一套灵活的机制,使得用户运行不同的应用时,使用相同的串行驱动。底层的物理驱动和tty驱动完成从硬件上收发数据,而线路规程则负责处理这些数据,并在内核空间和用户空间之间进行数据的传递。
串行子系统支持17种标准的线路规程。当你打开串口时系统会绑定默认的线路规程N_TTY,它实现了终端I/O处理。N_TTY负责加工从键盘接收到的字符。根据用户需要,它完成控制字符到“新起一行”的映射,进行小写字符至大写字符的转换,将tab、echo字符传递给关联的虚拟终端。N_TTY也支持原始编辑模式,此时,它将所有前述的处理都交给用户程序。下一章《输入设备驱动》中的图7.3展示了键盘子系统如何和N_TTY相关联。前一节“TTY驱动”中列出的tty驱动例子默认就是使用N_TTY。
线路规程也实现通过串行传输协议的网络接口。PPP(N_PPP)和SLIP(N_SLIP)子系统中的线路规程完成将包组帧、分配相应的网络数据结构并将数据传送至相应的网络协议栈的工作。其它的线路规程还包括红外数据(N_IRDA)和蓝牙主机控制接口(N_HCI)。
设备例子:触摸控制器
本节通过实现一个简单的串行触摸屏控制器的线路规程,来深入线路规程的内幕。图6.6显示了触摸控制器和嵌入式掌上电脑的连接。由于触摸控制器的有限状态机(FSM)能够很好地描述串行层提供的接口和功用,因此将可根据它实现线路规程。
图 6.6. PC-derivative上触摸控制器连接图

Open与Close
为了创建线路规程,需要定义tty_ldisc结构体,并向内核注册指定的入口函数集。清单6.2包括了实现了以上功能的触摸控制器实例的部分代码。
清单 6.2. 线路规程操作
Code View:
struct tty_ldisc n_touch_ldisc = {
TTY_LDISC_MAGIC,
"n_tch",
N_TCH,
n_touch_open,
n_touch_close,
n_touch_flush_buffer,
n_touch_chars_in_buffer,
n_touch_read,
n_touch_write,
n_touch_ioctl,
NULL,
n_touch_poll,
n_touch_receive_buf,
n_touch_receive_room,
n_touch_write_wakeup
};
if ((err = tty_register_ldisc(N_TCH, &n_touch_ldisc))) {
return err;
}
|
在清单6.2中,n_tch是线路规程名,N_TCH是线路规程的ID号。你需要在include/linux/tty.h中定义其值(此头文件中包括所有线路规程的定义)。在/proc/tty/ldiscs可发现正使用的线路规程。
线路规程从tty的flip缓冲区对应的部分收集、处理数据,然后拷贝处理后的数据至本地读缓冲区。对于N_TCH,n_touch_receive_room()返回读缓冲区中的剩余内存数,n_touch_chars_in_buffer()返回读缓冲区中已经处理过的、准备送至用户空间的字符个数。由于N_TCH是只读设备,n_touch_write() 和 n_touch_write_wakeup()将不进行任何操作。n_touch_open()用于为线路规程的主要数据结构分配内存,可参照清单6.3。
清单6.3. 打开线路规程
Code View:
struct n_touch {
int current_state;
spinlock_t touch_lock;
struct tty_struct *tty;
} *n_tch;
static int
n_touch_open(struct tty_struct *tty)
{
if (!(n_tch = kmalloc(sizeof(struct n_touch), GFP_KERNEL))) {
return -ENOMEM;
}
memset(n_tch, 0, sizeof(struct n_touch));
tty->disc_data = n_tch;
tty->read_buf = kmalloc(BUFFER_SIZE, GFP_KERNEL);
if (!tty->read_buf) return -ENOMEM;
memset(tty->read_buf, 0, BUFFER_SIZE);
spin_lock_init(&ntch->touch_lock);
return 0;
}
|
当打开连有触摸控制器的串口时,你可能想将N_TCH而非N_TTY设置为默认的线路规程,“改变线路规程”一节中介绍的方法可以实现从用户空间改变线路规程的目的。
读数据过程
对于中断驱动的设备,读取数据的过程通常由一前一后两个线程组成:
- 由于发起读数据请求而从用户空间发起的顶层线程;
- 由中断处理程序(接收来自设备的数据)唤醒的底层线程。
图6.7显示了这两个与读数据流程相关的线程。中断处理程序将receive_buf()(在我们的例子中是n_touch_receive_buf())函数当作任务进行排队。通过设置tty->low_latency可重载这一行为。
图 6.7. 线路规程读数据过程

在触摸控制器的数据手册详细描述了触摸控制器和处理器之间的专用通信协议,而驱动则用前面讨论过的有限状态机来实现此协议。清单6.4中将有限状态机作为receive_buf()入口点n_touch_receive_buf()的一部分。
清单 6.4. n_touch_receive_buf() 方法
Code View:
static void
n_touch_receive_buf(struct tty_struct *tty,
const unsigned char *cp, char *fp, int count)
{
…………………………………………………………………………
switch (tty->disc_data->current_state) {
case RESET:
tty->driver->write(tty, 0, mode_stream_command,
sizeof(mode_stream_command));
tty->disc_data->current_state = STREAM_DATA;
break;
case STREAM_DATA:
break;
case PARSING:
tty->disc_data->current_state = PARSED;
break;
case PARSED:
}
…………………………………………………………………………
if (tty->disc_data->current_state == PARSED) {
spin_lock_irqsave(&tty->disc_data->touch_lock, flags);
for (i=0; i < PACKET_SIZE; i++) {
tty->disc_data->read_buf[tty->disc_data->read_head] =
tty->disc_data->current_pkt[i];
tty->disc_data->read_head =
(tty->disc_data->read_head + 1) & (BUFFER_SIZE – 1);
tty->disc_data->read_cnt++;
}
spin_lock_irqrestore(&tty->disc_data->touch_lock, flags);
}
}
|
n_touch_receive_buf() 处理从串行驱动来的数据。它和触摸控制器进行一系列命令/响应的交互,将接收的坐标和压下/释放信息放入线路规程读缓冲区。对读缓冲区的访问必须借助自旋锁的加锁能力来依次进行,如图6.7所示,该自旋锁被ldisc.receive_buf() 和 ldisc.read() 线程(在我们的例子中分别是n_touch_receive_buf() 和 n_touch_read())同时使用。如清单6.4,n_touch_receive_buf()通过直接调用串行驱动的write()入口函数将命令分发给触摸控制器。
n_touch_receive_buf()需要做如下操作:
- 如果没有数据可获得,图6.7中的顶层的read()线程会将调用进程置为休眠状态。因此n_touch_receive_buf()必须将其唤醒,使其读取刚处理过的数据。
- 如果线路规程耗尽了读缓冲空间,n_touch_receive_buf()必须要求串行驱动中止从设备接收数据。当它将数据搬移至用户空间并释放读缓冲区中的内存后,ldisc.read()负责重新开启数据接收。串行驱动利用软件或硬件流控机制完成数据接收的中止和重启。
清单6.5实现了上面的操作。
清单 6.5. 唤醒读线程和中止串行驱动
if (waitqueue_active(&tty->read_wait) &&
(tty->read_cnt >= tty->minimum_to_wake))
wake_up_interruptible(&tty->read_wait);
}
if (n_touch_receive_room(tty) < TOUCH_THROTTLE_THRESHOLD) {
tty->driver.throttle(tty);
}
|
等待队列tty->read_wait用于在ldisc.read()和ldisc.receive_buf()线程之间实现同步。当ldisc.read()未发现可读的数据时,将调用进程加入等待队列,当有数据可读时,ldisc.receive_buf()唤醒ldisc.read()线程。因此,n_touch_read()完成如下操作:
· 当仍然无可读数据时,将调用进程放入read_wait队列中使其休眠。当数据到来时由n_touch_receive_buf()唤醒此进程。
· 若数据可获得,从本地读缓冲区(tty->read_buf[tty->read_tail])中收集数据,并分发至用户空间。
· 若串行驱动被中止,并且在读操作后读缓冲区中又有了足够的可用空间,请求串行驱动重启。
网络线路规程通常分配sk_buff(第15章“网络接口卡”中将讨论到的基本的Linux网络数据结构),并用它作读缓冲区。由于网络线路规程的receive_buf()将接收的拷贝至sk_buff并将其直接传送至相应的协议栈,因此它没有read()函数。
写数据过程
线路规程的write()入口函数需要完成一些数据传送至底层驱动之前必要的后处理工作。
如果底层驱动不能接受线路规程提供的所有数据,线路规程会将请求发送数据的线程置于休眠状态。当驱动准备好接受更多的数据时,驱动的中断处理例程将线路规程唤醒。为了达成此目的,驱动调用由线路规程注册的write_wakeup()方法。类似于前面章节中讨论过的read_wait实现同步,此处通过等待队列tty->write_wait来实现相应的同步。
很多网络线路规程没有write()方法。协议实现时直接将数据帧向下传送给串行设备驱动。然而,这些线路规程通常仍然有write_wakeup()入口点,以响应串行驱动的传输更多数据的请求。
因为触摸控制器是只读设备,N_TCH也没有write()方法。正如在清单6.4中所见,当需要发送命令帧给控制器时,接收路径中的例程直接和底层的UART驱动交互。