TTY概念解析
串口终端(/dev/ttyS*)
- 串口终端是使用计算机串口连接的终端设备;Linux以字符设备来处理这种串行端口;这些端口所对应的设备名称是/dev/ttySAC0-N
控制台终端(/dev/console )
- 计算机的输出设备通常被称为控制台终端,特指printk信息输出到的设备;/dev/console是一个虚拟设备;它需要映射到真正的tty上,这可以在内核启动参数中配置
虚拟终端(/dev/tty*)
- 用户登录时,使用的是虚拟终端 ,tty0是当前使用虚拟终端的别名
TTY子系统架构
-
tty 核心:为tty驱动提供接口,隔离上层应用与底层硬件
-
tty线路规程:加工与tty驱动交互的数据(数据的格式化等),勾勒串行层的行为,有助于复用底层的代码来支持不同的技术
i. N_TTY —–> /dev/ttySX (终端)
ii. N_IRDA—-> /dev/ircommX (红外)
iii. N_PPP —–> ppp0
-
tty驱动:关注uart或者其他底层串行硬件特征的驱动程序
-
串行子系统有提供一些内核API
架构图
回溯函数:dump_stack()
- 使用方法:将该函数加入到要回溯的函数中去,之后内核启动会自动串口打印回溯信息
串口驱动重要数据结构:
-
struct uart_driver:一个结构表示一个驱动;驱动能支持多个串口
-
struct uart_port:一个结构表示一个实在的串口:如串口0,串口1
-
struct uart_ops:串口操作集合,TTY最终调用的读写功能都在里面定义关联函数
串口驱动为了将自身和内核联系起来,必须完成两个步骤:
-
通过调用:uart_register_driver(struct uart_driver *);向串行核心注册
-
通过调用:uart_add_one_port(struct uart_driver*,struct uart_port *),注册其支持的每个端口
传统的UART使用TTY驱动程序为:/driver/serial/serial_core.c
USB-串行端口转换器的TTY驱动程序在目录:driver/usb/serial/usb-serial.c
TTY架构驱动追踪分析
分析思路
-
TTY属于字符设备
-
TTY为分层架构
初始化设备
-
串口在CPU启动时引导向内核注册为平台设备
-
设备驱动初始化在prob函数进行
-
初始化需要完成以下内容:
-
获取端口(一个端口就为一个串口)
-
初始化端口
-
添加端口
-
向内核申请私有空间
-
创建属性设备文件
-
提供调频支持
-
打开设备:上层调用到底层的历程:
上层应用
a) 思路:用户打开open函数;系统需要寻找对应的file_operations
b) file_operations 是由谁注册的?驱动中找找
底层驱动
c) 查看驱动只有uart_register_driver();
TTY核心 tty_io.c
-
进入查看,串口驱动向tty核心注册了tty_driver:tty_register_driver(normal)
-
查看tty_register_driver(): 在这里注册了字符设备
cdev_init(&driver->cdev, &tty_fops);
cdev_add(&driver->cdev, dev, driver->num);
-
注册字符设备对应的操作函数集在
-
&tty_fops –> static const struct file_operations tty_fops
-
tty_fops.open
-
tty_open(struct inode *inode, struct file *filp)
-
在tty_open函数中对struct tty_operations uart_ops进行操作,通过下面语句
-
retval = tty->ops->open(tty, filp); 通过这层调用进入TTY驱动层
-
TTY驱动 serial_core.c
-
跟踪 struct tty_operations uart_ops结构体中的open函数
-
进入static int uart_open(struct tty_struct *tty, struct file *filp)
-
该函数又调用 uart_startup()函数
-
该函数通过retval = uport->ops->startup(uport);进入驱动层
底层驱动
-
最终调用到驱动程序中的struct uart_ops中的操作集
-
int (*startup)(struct uart_port *);
总结
用户应用 的 open 通过层层调用到达驱动函数中的 xxx_startup(…);
在xxx_startup函数中需要完成的任务
-
使能串口接收功能:rx_enabled(port) = 1;
-
注册数据接收中断处理程序:request_irq();
-
使能发送功能:tx_enabled(port) = 1;
-
注册发送中断处理函数:request_irq();
-
出错处理
写设备
a) 思路:用户打开write函数;系统需要寻找对应的file_operations
b) file_operations 是由谁注册的?驱动中找找
TTY核心-tty_io.c
-
进入查看,串口驱动向tty核心注册了tty_driver:tty_register_driver(normal)
-
查看tty_register_driver():在这里注册了字符设备
cdev_init(&driver->cdev, &tty_fops);
cdev_add(&driver->cdev, dev, driver->num);
-
注册字符设备对应的操作函数集在
-
&tty_fops –> static const struct file_operations tty_fops
-
tty_fops.write函数
-
tty_write(struct inode *inode, struct file *filp)
-
在该函数中调用了:do_tty_write(ld->ops->write, tty, file, buf, count)
-
TTY线路规程tty_ldisc.c n_tty.c
-
以上传递进来的是ld->ops == struct tty_ldisc_ops *ops;
-
ops->write 调用到struct tty_ldisc_ops tty_ldisc_N_TTY.write函数
-
在n_tty_write函数中对struct tty_operations uart_ops进行操作,通过下面语句
-
tty->ops->write(tty, b, nr);通过这层调用进入TTY驱动层
TTY驱动serial_core.c
-
跟踪 struct tty_operations uart_ops结构体中的write函数
-
进入uart_write
-
该函数又调用了uart_ops结构中的uart_startup()函数
-
接着再调用void __uart_start(struct tty_struct *tty)
-
之后再进过指针传递调用驱动程序
底层驱动
- 最终调用到驱动程序中的struct uart_ops中的操作集
- 发送函数:
void (*start_tx)(struct uart_port *);
发送函数的工作
-
使能发送中断
-
具体发送在中断函数中进行
-
判断x_char 是否为0,不为0则发送x_char (x_char用来通知数据缓存是否忙碌)
-
判断发送换从是否为空:uart_circ_empty();或者是驱动设置为停止发送状态:uart_tx_stoped(),则取消发送
-
循环发送直到循环缓冲为空
-
发送FIFO满,退出发送
-
将要发送的数据写入发送寄存器
-
修改循环缓冲位置
-
-
如果发送缓冲有空闲空间,则唤醒发送进程:uart_wake_up();
-
如果发送缓冲为空,则关闭发送使能:uart_tx_stoped();
-
设备读
-
同理从用户层到驱动,最终调用到驱动的
其中要注意的是线路规程中的n_tty_read函数,不直接操作底层驱动的串口缓存,需要通过tty->read_buf[tty->read_tail];其中的数据是串口驱动通过发送函数(tty_push将数据推送到该buf中)
-
驱动中的发送函数处理流程
while(max_count-- > 0) /*这个用来平衡系统的性能*/
{
读取UPFCON寄存器rd_regl();
读取UFSTAT寄存器 rd_regl();
通过以上读取的数据判断FIFO缓存是否为空,空则退出循环
读取UERSTAT寄存器
读取URXH,从该寄存器中取出字符
流控处理
根据UFSTAT记录错误类型
如果收到的是sysrq字符,进行特出处理,调用内核函数:
1. uart_handle_sysrq_char(port, ch)
把接收到的字符送到串口驱动的uart_insert_char();
}
最后一步:将串口缓存中的数据推送到tty->read_buf中tty_flip_buffer_push();
流控:Linux使用的是自动硬件流控 或者是 软件流控–>使用x_char来通讯标识