主要是想对Linux 串口、usb转串口驱动框架有一个整体的把控,因此会忽略某些细节,同时里面涉及到的一些驱动基础,比如字符设备驱动、平台驱动等也不进行详细说明原理。如果有任何错误地方,请指出,谢谢!
声明:图和个别段落(我做了小的修改)是直接从网上截取
linux下的串口或者usb转串口驱动都是依赖linux内核提供的tty核心、tty线路规划和tty驱动,所以牵涉到很多层次,之所以有这么多层次,肯定是有它们存在意义的。
举例来说,像串口或者usb转串口的驱动,最终可以确定的是以字符设备驱动提供给上层使用,于是tty核心层就对这部分通用的实现进行了封装,但这不是最重要的,最重要的是tty核心层里同时实现了一种数据格式化机制,这就是tty线路规划,这样的好处是可以分别针对不同类设备的线路规划,比如针对终端io的,比如针对网络的ppp还有slip还有蓝牙还有IrDA等,这些的实现不需要考虑底层硬件,也就是说这些串口到具体协议的转换的实现与硬件相分离了,这就是tty核心及tty线路规划存在的目地。
那为什么会有tty驱动层呢? 也许你觉得我们的串口驱动可以直接通过tty核心提供的功能就可以实现了。 这个确实是可以,但是linux内核因为要兼容世界上存在的各种串口设备,所以针对串口额外实现了一个serial核心层,针对usb转串口额外实现了usb-serial核心层,它们就是所谓的tty驱动层。我们的串口或者usb转串口实现就是与tty驱动层打交道,当然串口芯片或者usb转串口芯片有很多种,所以不同的芯片都要有对应的驱动,但是它们都是基于tty驱动层实现,这个是可以肯定的。
所以,我们要写串口驱动,最好还是对这些层次有些了解。
整体框架图如下:
这图是直接摘抄网上的。其实,我认为在tty驱动层下还应该有个具体的驱动,比如如果是8250串口控制器芯片,那么应该有个8250的驱动,然后才是硬件。
更准确的图我认为如下图所示:
linux串口驱动实现框架图:
下面摘抄网上的,主要简单介绍了上图,写的比较简明、清晰
1. tty线程规程
以特殊的方式格式化从一个用户或者硬件收到的数据,这种格式化常常采用一个协议转换的形式,如虚拟终端、PPP、Bluetooth、Ir等。
2. tty设备发送数据流程
tty核心从一个用户获取将要发送给一个tty设备的数据,tty核心将数据传递给tty线路规程驱动,接着数据被传递到tty驱动,tty驱动将数据转换为可以发送的硬件格式。
3. tty设备接收数据流程
从tty硬件接收到的数据向上交给tty驱动,进入tty线路规程驱动,再进入tty核心,在此被用户获取。尽管tty核心与tty之间的数据传输会经历tty线路规程的转换,但是tty驱动与tty核心之间也可以直接传输数据。
再摘抄2张网上的图:
tty设备的数据流通图:
tty在linux下属于字符设备驱动,tty层提供了一些数据结构和函数接口方便其他驱动注册上来,其中包括虚拟终端、串口终端、伪终端等。Tty核心部分在tty_io.c
里面实现。
第一步、内核默认的tty初始化部分
static int __init tty_class_init(void)
{
tty_class = class_create(THIS_MODULE, "tty");
if (IS_ERR(tty_class))
return PTR_ERR(tty_class);
tty_class->devnode = tty_devnode;
return 0;
}
postcore_initcall(tty_class_init);
上面代码创建了tty类,方便以后创建设备节点,然后是tty_init
,tty_init
函数负责初始化tty层,它是由chr_dev_init
调用的(fs_initcall(chr_dev_init))
,也就是说它属于字符设备一部分。
int __init tty_init(void)
{
cdev_init(&tty_cdev, &tty_fops);
if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0)
panic("Couldn't register /dev/tty driver\n");
device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 0), NULL,
"tty");
cdev_init(&console_cdev, &console_fops);
if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0)
panic("Couldn't register /dev/console driver\n");
device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 1), NULL,
"console");
#ifdef CONFIG_VT
vty_init(&console_fops);
#endif
return 0;
}
注:个人认为上面的if判断写法不是很好,虽然是正确的 这里和我们最终关心的串口驱动没关系,但由此可以看出tty字符设备(/dev/tty)使用的主设备号是TTYAUX_MAJOR
(5),次设备号为0,/dev/console使用的主设备号也是5,但次设备号为1,控制台的初始化console_init
在这个函数之前会被调用(start_kernel
),内核注释如下:
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
console_init();
这里不跟进去分析了。 虚拟终端、控制台部分暂时忽略不管。
第二步:使用tty层提供的功能(我们只关心串口驱动,所以是serial核心层或者usb-serial
核心层使用它们),主要包含
1)tty_register_driver
注册tty驱动
相关数据结构:struct tty_driver *driver
可以通过alloc_tty_driver
分配,它主要任务是
1. 创建一个字符设备,但是这个字符设备的操作集是tty层定义的tty_fops
,之所以由tty层提供,是因为它要实现线路规划部分,数据流会由它转向线路规划部分中。
static const struct file_operations tty_fops = {
.llseek = no_llseek,
.read = tty_read,
.write = tty_write,
.poll = tty_poll,
.unlocked_ioctl = tty_ioctl,
.compat_ioctl = tty_compat_ioctl,
.open = tty_open,
.release = tty_release,
.fasync = tty_fasync,
};
这其实是起到一个桥接作用。后面再分析这点
2. 将该驱动对象加入到全局的链表。这一步就是为了上面说的桥接。
2)tty_register_device
注册tty设备,只需要指定对应的驱动对象和索引号即可。它创建一个字符设备到/dev下 设备号由驱动对应的设备号base+索引。
下面以几个情景分析(这里只分析tty框架的处理,还没有和具体的驱动挂钩):
在应用层open上文第二步中tty_register_device
创建的设备,会经过vfs 最终到tty_init
中注册的tty_fops
操作集里的open,也就是tty_open
。它会根据你打开的是/dev/tty 还是 /dev/console 或者是你自己定义的一个设备(比如串口设备)(这个是由你tty_register_driver
注册是参数struct tty_driver *driver
里面的major决定的)
这里假设打开的是自己定义的设备/dev/ttyS0,那么会通过 driver = get_tty_driver(device, &index);
获取,它其实是扫描全局链表,这个链表的建立是在第二步中第2小步说明部分完成的。
如果是第一次打开,那么会创建一个新的对象用来代表这个open及以后操作的上下文,即tty_struct
,通过alloc_tty_struct
分配的,它里面有相应的线路规划策略tty_ldisc_init
,默认初始化为tty_ldisc_get(N_TTY)
。 然后调用线路规划的open。tty_struct
对象同时继承了driver的操作集tty_fops
,它内部同时会分配并初始化ktermios对象tty_init_termios(tty)
及在driver上登记driver->ttys[idx] = tty; 最后会调用驱动本身注册的open。 tty_struct
对象会放到file的private_data
,为以后操作做好准备。
在应用层read上文第二步中tty_register_device
创建的设备,会经过vfs 最终到tty_init中注册的tty_fops
操作集里的read 也就是tty_read
tty设备没有read函数,是因为大部分tty的输入设备和输出设备不一样。例如我们的虚拟终端设备,它的输入是键盘,输出是显示器。
由于这样的原因,tty的驱动层和tty的线路规程层都有一个缓冲区。 tty驱动层的缓冲区用来保存硬件发过来的数据。在驱动程序里使用 tty_insert_flip_string
函数可以实现将硬件的数据存入到驱动层的缓冲区。
其实一个缓冲区就够了,为什么线路规程层还是有一个缓冲区呢? 那是因为tty核心无法直接读取驱动层的缓冲区的数据。tty核心读不到数据,用户也就无法获取数据。用户的read函数只能从tty核心读取数据。而tty核心只能从tty线路规程层的缓冲区读取数据。 因为是层层读写的关系,所以tty线路规程也是需要一个缓冲区的。 在驱动程序里使用tty_flip_buffer_push()
函数将tty驱动层缓冲区的数据推到tty线路规程层的缓冲区。这样就完成了数据的流通。 因为全是缓冲区操作,所以需要两个进程:写数据进程和读数据进程。
如果缓冲区内没有数据,运行读进程的话,tty核心就会把读进程加入到等待队列。
tty_read
的主要流程: 从上文分析的open函数所存储的private里面取出分配并初始化过的tty_struct
对象tty = (struct tty_struct *)file->private_data;
,然后它会调用属于tty的线路规划里面的read,线路规划是通过tty_register_ldisc
注册到一个全局数组里的,对应默认的线性规划是文件tty_ldisc.c
里面tty_ldisc_begin
完成的,它是在console_init
里被调用的,也就是内核调用tty_init
之前。
void tty_ldisc_begin(void)
{
/* Setup the default TTY line discipline. */
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
}
struct tty_ldisc_ops tty_ldisc_N_TTY = {
.magic = TTY_LDISC_MAGIC,
.name = "n_tty",
.open = n_tty_open,
.close = n_tty_close,
.flush_buffer = n_tty_flush_buffer,
.chars_in_buffer = n_tty_chars_in_buffer,
.read = n_tty_read,
.write = n_tty_write,
.ioctl = n_tty_ioctl,
.set_termios = n_tty_set_termios,
.poll = n_tty_poll,
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup
};
因此最终调用n_tty_read
,它会根据是否有数据做不同的处理,如果有数据,则直接处理后返回,如果没有数据,那么就在等待队列上睡眠等待。
在应用层write上文第二步中tty_register_device
创建的设备,会经过vfs 最终到tty_init
中注册的tty_fops
操作集里的write 也就是tty_write
。Write调用要简单很多,它调用do_tty_write
, 它内部实际调用的是线路规划的n_tty_write
,它当然会调用tty_struct
的write,也就是继承自tty驱动的write c = tty->ops->write(tty, b, nr);
由驱动完成最终的操作硬件发送数据。
注意:这里描述的读、写是以终端io为例,如果是蓝牙、或者ppp这些网络io,read、write会通过网络协议栈,而不是这里的tty_read tty_write
。
第二步、具体驱动部分分析 1、 serial核心层(tty驱动层实现)分析
2、 串口驱动分析(8250为例)
1、 usb-serial核心层(tty驱动层实现)分析
2、 usb转串口驱动分析(pl2303为例)
另外再上两张图
------------------未完,待续!