前言

  主要是想对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的驱动,然后才是硬件。

更准确的图我认为如下图所示:

更详细的如下图所示:

下面摘抄网上的,主要简单介绍了上图,写的比较简明、清晰

  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框架分析

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_inittty_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框架的处理,还没有和具体的驱动挂钩):

情景1:打开设备

在应用层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,为以后操作做好准备。

情景2:从设备读数据

在应用层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,它会根据是否有数据做不同的处理,如果有数据,则直接处理后返回,如果没有数据,那么就在等待队列上睡眠等待。

情景2:从设备写数据

在应用层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为例)

另外再上张图

------------------未完,待续!

       2014年5月

linux驱动基础系列--Linux 串口、usb转串口驱动分析的更多相关文章

  1. linux驱动基础系列--linux spi驱动框架分析

    前言 主要是想对Linux 下spi驱动框架有一个整体的把控,因此会忽略某些细节,同时里面涉及到的一些驱动基础,比如平台驱动.设备模型等也不进行详细说明原理.如果有任何错误地方,请指出,谢谢! spi ...

  2. linux驱动基础系列--linux spi驱动框架分析(续)

    前言 这篇文章是对linux驱动基础系列--linux spi驱动框架分析的补充,主要是添加了最新的linux内核里设备树相关内容. spi设备树相关信息 如之前的文章里所述,控制器的device和s ...

  3. linux驱动基础系列--Linux下Spi接口Wifi驱动分析

    前言 本文纯粹的纸上谈兵,我并未在实际开发过程中遇到需要编写或调试这类驱动的时候,本文仅仅是根据源码分析后的记录!基于内核版本:2.6.35.6 .主要是想对spi接口的wifi驱动框架有一个整体的把 ...

  4. linux驱动基础系列--Linux mmc sd sdio驱动分析

    前言 主要是想对Linux mmc子系统(包含mmc sd sdio)驱动框架有一个整体的把控,因此会忽略某些细节,同时里面涉及到的一些驱动基础,比如平台驱动.块设备驱动.设备模型等也不进行详细说明原 ...

  5. linux驱动基础系列--Linux I2c驱动分析

    前言 主要是想对Linux I2c驱动框架有一个整体的把控,因此会忽略协议上的某些细节,同时里面涉及到的一些驱动基础,比如平台驱动.设备模型.sysfs等也不进行详细说明原理,涉及到i2c协议部分也只 ...

  6. linux驱动基础系列--linux rtc子系统

    前言 linux驱动子系统太多了,连时钟也搞了个子系统,这导致一般的时钟芯片的驱动也会涉及到至少2个子系统,一个是时钟芯片接口子系统(比如I2c接口的时钟芯片),一个是内核给所有时钟芯片提供的rtc子 ...

  7. Linux 串口、usb转串口驱动分析(2-2) 【转】

    转自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=26807463&id=4186852 Linux 串口.usb转 ...

  8. Linux 串口、usb转串口驱动分析(2-1) 【转】

    转自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=26807463&id=4186851 Linux 串口.usb转 ...

  9. STM32 USB转串口驱动安装不成功出现黄色感叹号解决方法!

    相信很多人在做USB转串口时出现过串口驱动安装不成功,出现黄色感叹号问题, 出现这种问题一般是驱动安装不成功造成的. 这里我就这个问题总结几个简单的方法. 方法1: 插上USB,利用驱动人生安装驱动. ...

随机推荐

  1. JAVA开发工作流程

    阶段 0 :拟出一个计划 决定在后面的过程中采取哪些步骤,思考整个开发任务如何实现,分步骤建立“路标”,这样可以帮助自己开发时一步一个脚印的逐步完成,有效的防止自己在开发过程中迷失方向. 阶段 1 : ...

  2. matlab 中try/catch语句

    try的作用是让Matlab尝试执行一些语句,执行过程中如果出错,则执行catch部分的语句,其语法: try (command1)组命令1总被执行,错误时跳出此结构 catch (command2) ...

  3. vijos1859[TJOI2014]电源插排

    题意:小 M 的实验室有很多电源插排.这些插排的编号从 1 到 N,由左向右排成一排.每天早晨,这些插排都是没有被使用的.每当一个学生来到实验室,他就将自己的笔记本电源插到某一个未被使用的插排上.实验 ...

  4. 【bzoj5108】[CodePlus2017]可做题 拆位+乱搞

    题目描述 给出一个长度为 $m$ 的序列 $a$ ,编号为 $a_1\sim a_m$,其中 $n$ 个位置的数已经确定,剩下的位置的数可以任意指定.现在令 $b$ 表示 $a$ 的前缀异或和,求 $ ...

  5. 【bzoj2100】[Usaco2010 Dec]Apple Delivery 最短路

    题目描述 Bessie has two crisp red apples to deliver to two of her friends in the herd. Of course, she tr ...

  6. 【刷题】洛谷 P2709 小B的询问

    题目描述 小B有一个序列,包含N个1~K之间的整数.他一共有M个询问,每个询问给定一个区间[L..R],求Sigma(c(i)^2)的值,其中i的值从1到K,其中c(i)表示数字i在[L..R]中的重 ...

  7. [POI2014]DOO-Around the world

    通过几年的努力,Byteasar最终拿到了飞行员驾驶证.为了庆祝这一事实,他打算买一架飞机并且绕Byteotia星球赤道飞行一圈.但不幸的是赤道非常长所以需要中途加几次油.现在已知赤道上面所有飞机场, ...

  8. javascript实用例子

    js学习笔记,别错过!很有用的. /////////////////////////////////////////////////////////////////////////////////// ...

  9. POJ 3579 二分

    Median Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 7687   Accepted: 2637 Descriptio ...

  10. git branch 重命名

    有时候你会有重命名一个git branch的冲动,不要怀疑,这是真的.command bellow will give u a big help,no thanks~ git branch - m o ...