串行设备驱动模型

Hntea bio photo By Hntea

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

架构图

tty1

tty2

回溯函数:dump_stack()

  • 使用方法:将该函数加入到要回溯的函数中去,之后内核启动会自动串口打印回溯信息

串口驱动重要数据结构:

  • struct uart_driver:一个结构表示一个驱动;驱动能支持多个串口

  • struct uart_port:一个结构表示一个实在的串口:如串口0,串口1

  • struct uart_ops:串口操作集合,TTY最终调用的读写功能都在里面定义关联函数

串口驱动为了将自身和内核联系起来,必须完成两个步骤:

  1. 通过调用:uart_register_driver(struct uart_driver *);向串行核心注册

  2. 通过调用: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

  1. 进入查看,串口驱动向tty核心注册了tty_driver:tty_register_driver(normal)

  2. 查看tty_register_driver(): 在这里注册了字符设备

	cdev_init(&driver->cdev, &tty_fops);
	cdev_add(&driver->cdev, dev, driver->num);
  1. 注册字符设备对应的操作函数集在

    • &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

  1. 跟踪 struct tty_operations uart_ops结构体中的open函数

  2. 进入static int uart_open(struct tty_struct *tty, struct file *filp)

  3. 该函数又调用 uart_startup()函数

  4. 该函数通过retval = uport->ops->startup(uport);进入驱动层


底层驱动

  1. 最终调用到驱动程序中的struct uart_ops中的操作集

  2. int (*startup)(struct uart_port *);


总结

用户应用 的 open 通过层层调用到达驱动函数中的 xxx_startup(…);

在xxx_startup函数中需要完成的任务

  1. 使能串口接收功能:rx_enabled(port) = 1;

  2. 注册数据接收中断处理程序:request_irq();

  3. 使能发送功能:tx_enabled(port) = 1;

  4. 注册发送中断处理函数:request_irq();

  5. 出错处理

写设备

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

  1. 以上传递进来的是ld->ops == struct tty_ldisc_ops *ops;

  2. ops->write 调用到struct tty_ldisc_ops tty_ldisc_N_TTY.write函数

  3. 在n_tty_write函数中对struct tty_operations uart_ops进行操作,通过下面语句

  4. tty->ops->write(tty, b, nr);通过这层调用进入TTY驱动层

TTY驱动serial_core.c

  1. 跟踪 struct tty_operations uart_ops结构体中的write函数

  2. 进入uart_write

  3. 该函数又调用了uart_ops结构中的uart_startup()函数

  4. 接着再调用void __uart_start(struct tty_struct *tty)

  5. 之后再进过指针传递调用驱动程序

底层驱动

  1. 最终调用到驱动程序中的struct uart_ops中的操作集
  2. 发送函数: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();

设备读

  1. 同理从用户层到驱动,最终调用到驱动的

    其中要注意的是线路规程中的n_tty_read函数,不直接操作底层驱动的串口缓存,需要通过tty->read_buf[tty->read_tail];其中的数据是串口驱动通过发送函数(tty_push将数据推送到该buf中)

  2. 驱动中的发送函数处理流程

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来通讯标识