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