# UART 子系统 ## UART物理特性 ### 参数 - 波特率:一般选波特率都会有9600,19200,115200等选项。其实意思就是每秒传输这么多个比特位数(bit)。 - 起始位:先发出一个逻辑”0”的信号,表示传输数据的开始。 - 数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输。 - 校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。 - 停止位:它是一个字符数据的结束标志 ![](media/image-20221207142543760.png) ### 时序 逻辑电平0和1在不同的设备上定义不同 - TTL/CMOS逻辑电平 在xV至5V之间,就认为是逻辑1,在0V至yV之间就为逻辑0 ![](media/image-20221207142700248.png) - RS-232逻辑电平 在-12V至-3V之间,就认为是逻辑1,在+3V至+12V之间就为逻辑0 ,RS-232的电平比TTL/CMOS高,能传输更远的距离,在工业上用得比较多。 ![](media/image-20221207142746200.png) ### 内部逻辑图 ![](media/image-20221207142949696.png) 1. 要发送数据时,CPU控制内存要发送的数据通过FIFO传给UART单位,UART里面的移位器,依次将数据发送出去,在发送完成后产生中断提醒CPU传输完成。 2. 接收数据时,获取接收引脚的电平,逐位放进接收移位器,再放入FIFO,写入内存。在接收完成后产生中断提醒CPU传输完成。 ## TTY体系设备节点 | 设备节点 | 含义 | | ----------------------------------- | -------------------------------------------- | | /dev/ttyS0、/dev/ttySAC0 | 串口 | | /dev/tty1、/dev/tty2、/dev/tty3、…… | 虚拟终端设备节点 | | /dev/tty0 | 前台终端 | | /dev/tty | 程序自己的终端,可能是串口、也可能是虚拟终端 | | /dev/console | 控制台,又内核的cmdline参数确定 | TTY/Terminal/Console/UART,它们有什么差别? | 术语 | 含义 | | -------- | ------------------------------------------------------------ | | TTY | 来自teletype,最古老的输入输出设备,现在用来表示内核的一套驱动系统 | | Terminal | 终端,暗含远端之意,也是一个输入输出设备,可能是真实设备,也可能是虚拟设备 | | Console | 控制台,含控制之意,也是一种Terminal,权限更大,可以查看内核打印信息 | | UART | 串口,它的驱动程序包含在TTY驱动体系之内 | - Uart是TTY驱动体系的一种 - 一个设备可以开很多个Terminal,对应的底层可能是虚拟设备也可能是真是设备,比如ssh telet Console终端等等 - Console其实也属于一种Terminal,但是它的权限很大 由于历史原因,下图中两条红线之内的代码被称为TTY子系统。它既支持UART,也支持键盘、显示器,还支持更复杂的功能(比如伪终端) ![](media/image-20221207144455574.png) - `/dev/tty` 表示本程序的终端,可能是虚拟终端,也可能是真实的终端。 程序A在前台、后台间切换,它自己的/dev/tty都不会变。例如上图中的shell0程序的终端是`/dev/ttyS0` 实际shell0访问`/dev/tty`和访问`/dev/ttyS0` 效果一样 - `/dev/tty0` 表示前台程序的虚拟终端 * 你正在操作的界面,就是前台程序 * 其他后台程序访问`/dev/tty0`的话,就是访问前台程序的终端,切换前台程序时,`/dev/tty0`是变化的 ```shell // 1. 在tty3终端执行如下命令 // 2. 然后在tty3、tty4来回切换,你会发现切换到谁,msg_from_tty3字符串在谁上显示, while [ 1 ]; do echo msg_from_tty3 > /dev/tty0; sleep 5; done ``` - `/dev/ttyN (N=1 2 3···)` 表示某个程序使用的虚拟终端 ## TTY驱动框架体系 ### 行规程 ![](media/image-20221207145526675.png) 如上图所示Line Discipline就是行规程,为什么会有行规程? 大多数用户都会在输入时犯错,所以退格键会很有用。这当然可以由应用程序本身来实现,但是根据UNIX设计哲学,应用程序应尽可能保持简单。为了方便起见,操作系统提供了一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),这些命令在行规范内默认启用。高级应用程序可以通过将行规范设置为原始模式(raw mode)而不是默认的成熟或准则模式(cooked and canonical)来禁用这些功能。 大多数交互程序(编辑器,邮件客户端,shell,及所有依赖curses或readline的程序)均以原始模式运行,并自行处理所有的行编辑命令。行规范还包含字符回显和回车换行(译者注:\r\n 和 \n)间自动转换的选项。如果你喜欢,可以把它看作是一个原始的内核级sed(1)。 另外,内核提供了几种不同的行规范。一次只能将其中一个连接到给定的串行设备。行规范的默认规则称为`N_TTY`(`drivers/char/n_tty.c`)。其他的规则被用于其他目的,例如管理数据包交换(ppp,IrDA,串行鼠标),但这不在本文的讨论范围之内。 ### TTY驱动程序框架 ![](media/image-20221207145755039.png) ## 驱动注册流程分析 1. 驱动和设备树匹配过程 > 如下图所示:驱动加载或者设备树转换过程中相互匹配的过程 ![](media/image-20221208153910555.png) 上图的关注点 1. 在驱动的init函数中会调用`uart_register_driver`函数,注册一个`struct uart_driver`。 ![](media/image-20221208155422258.png) 注册过程如下: ```c static int __init imx_serial_init(void) { uart_register_driver(&imx_reg); { /* 1.分配nr个struct uart_state 实体 里面有struct tty_port 实体 * struct uart_state { * struct tty_port port; * enum uart_pm_state pm_state; * struct circ_buf xmit; * struct uart_port *uart_port; * }; * */ drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL); // 2.分配一个struct tty_driver 实体 normal = alloc_tty_driver(drv->nr); { tty_alloc_driver(lines, 0) { __tty_alloc_driver(lines, THIS_MODULE, flags) { // 2.1 这里分配空间struct tty_driver 并设置里面的成员变量 struct tty_driver *driver = kzalloc(sizeof(struct tty_driver), GFP_KERNEL); // 2.1 这里分配nr个 struct tty_struct * 槽 driver->ttys = kcalloc(lines, sizeof(*driver->ttys),GFP_KERNEL); // 2.2 这里分配nr个 struct ktermios * 槽 driver->termios = kcalloc(lines, sizeof(*driver->termios),GFP_KERNEL); // 2.3 这里分配nr个 struct tty_port *槽 driver->ports = kcalloc(lines, sizeof(*driver->ports),GFP_KERNEL); // 2.4 这里分配nr个 struct cdev 的实体 driver->cdevs = kcalloc(cdevs, sizeof(*driver->cdevs), GFP_KERNEL); } } } drv->tty_driver = normal; normal->driver_name = drv->driver_name; normal->name = drv->dev_name; normal->major = drv->major; normal->driver_state = drv; tty_set_operations(normal, &uart_ops); { //2.5 设置 struct tty_driver里面的ops操作函数 driver->ops = op; /* static const struct tty_operations uart_ops = { * .open = uart_open, * .close = uart_close, * .write = uart_write, * .put_char = uart_put_char, * .flush_chars = uart_flush_chars, * .write_room = uart_write_room, * } */ } // 3 前面在1的时候分配了nr个 struct tty_port 这里设置tty_port里面的ops指向 uart_port_ops for (i = 0; i < drv->nr; i++) { struct uart_state *state = drv->state + i; struct tty_port *port = &state->port; tty_port_init(port); { port->ops = &uart_port_ops; /* * static const struct tty_port_operations uart_port_ops = { * .activate = uart_port_activate, * .shutdown = uart_port_shutdown, * .carrier_raised = uart_carrier_raised, * .dtr_rts = uart_dtr_rts, * }; */ } } // 4 注册刚才在步骤2.4里面分配的nr个cdev结构体实体,这里注册字符设备驱动,ops指向tty_fops tty_register_driver(normal); { alloc_chrdev_region(&dev, driver->minor_start,driver->num, driver->name); tty_cdev_add(driver, dev, 0, driver->num); { cdev_init(&driver->cdevs[index], &tty_fops); driver->cdevs[index].owner = driver->owner; return cdev_add(&driver->cdevs[index], dev, count); } } } } ``` 整个过程形成的结果如下: ![image-20221209095419864](media/image-20221209095419864.png) 2. 如果设备树和驱动的compatible属性匹配调用probe函数,在probe函数中会生成一个`struct uart_port`结构体,且里面有个ops指针指向`imx_pops`,`imx_pops`里面存放着硬件操作串口底层寄存器的函数。 ![](media/image-20221209095953036.png) 3. 然后调用`uart_add_one_port`的函数,把`struct uart_port`注册 这个过程下面会分析。 2. 分析`uart_add_one_port`的函数 ```c uart_add_one_port(struct uart_driver *drv, struct uart_port *uport) { tty_dev = tty_port_register_device_attr(port, drv->tty_driver, uport->line, uport->dev, port, uport->tty_groups); { tty_port_link_device(port, driver, index); // 这里的index=uport->line { // 1.struct tty_driver *driver 里面的 struct tty_port *槽赋值为 // uart_state 里面的 struct tty_port实体 driver->ports[index] = port; } tty_register_device_attr(driver, index, device, drvdata,attr_grp); { // 这里的name就是我们看到的/dev/xxx 具体就是struct tty_driver里面的name // 也是struct uart_driver里面的name 参见uart_register_driver即可,对应到imx6就是 // #define DEV_NAME "ttymxc" tty_line_name(driver, index, name); { sprintf(p, "%s%d", driver->name,index + driver->name_base) } tty_cdev_add(driver, devt, index, 1); { cdev_init(&driver->cdevs[index], &tty_fops);// ---这里的tty_fops /* * 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, * }; */ driver->cdevs[index].owner = driver->owner; return cdev_add(&driver->cdevs[index], dev, count); } } } } ``` ## 驱动open过程分析 通过上面介绍,实际最终还是回到字符设备驱动那一套框架,当我们应用层open的时候,肯定会调用cdev里面的ops中的open函数,即tty_fops里面的open函数 ```c 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,// open 时候,第一个调用的函数接口 .release = tty_release, .fasync = tty_fasync, }; ``` 下面分析`tty_open`函数 ### tty_open ```c static int tty_open(struct inode *inode, struct file *filp) { struct tty_struct *tty; tty = tty_init_dev(driver, index); { tty = alloc_tty_struct(driver, idx); { tty->driver = driver; tty->ops = driver->ops;// 实际就是struct tty_driver 里面的ops即 /* static const struct tty_operations uart_ops = { * .open = uart_open, * .close = uart_close, * .write = uart_write, * .put_char = uart_put_char, * .flush_chars = uart_flush_chars, * .write_room = uart_write_room, * } */ tty->index = idx; tty_line_name(driver, idx, tty->name); tty->dev = tty_get_device(tty); } retval = tty_driver_install_tty(driver, tty); { driver->ttys[tty->index] = tty; //这个时候才为struct tty_driver里面struct tty_struct * 槽赋值 } tty->port = driver->ports[idx]; } if (tty->ops->open) retval = tty->ops->open(tty, filp); // 调用 uart_open函数 } ``` 这里面比较重要的就是分配设置`struct tty_struct *tty`其实这是一个中间过程,只有在我们打开一个串口的时候会生成,设置`struct tty_struct`是为了和先前设置的ops等挂钩,能找到对应的函数而已,比如下面的代码,往下在调用,就会跑到`uart_open`这一层了 ```c tty->ops = driver->ops;// 实际就是struct tty_driver 里面的ops即 /* static const struct tty_operations uart_ops = { * .open = uart_open, * .close = uart_close, * .write = uart_write, * .put_char = uart_put_char, * .flush_chars = uart_flush_chars, * .write_room = uart_write_room, * } */ ``` ### uart_open ```c static int uart_open(struct tty_struct *tty, struct file *filp) { uart_startup(tty, state, 0); { uart_port_startup(tty, state, init_hw); { // struct uart_state.struct uart_port*->ops = struct uart_ops imx_pops // 在uart_add_one_port函数里面赋值的state->uart_port = uport; uport->ops->startup(uport); { //这里就直接调用imx_pops->startup函数了 { /* 寄存器相关了 */ temp &= ~(UCR4_CTSTL_MASK << UCR4_CTSTL_SHF); temp |= CTSTL << UCR4_CTSTL_SHF; } } } } } ``` ## 驱动read过程分析 ### 行规程注册 文件:`drivers\tty\n_tty.c` ```c void __init n_tty_init(void) { tty_register_ldisc(N_TTY, &n_tty_ops); } ``` 以后可以通过标号N_TTY找到这个行规程。 ### open设备时确定行规程 ```c tty_open tty_open_by_driver tty_init_dev tty = alloc_tty_struct(driver, idx); tty_ldisc_init(tty); struct tty_ldisc *ld = tty_ldisc_get(tty, N_TTY); tty->ldisc = ld; ``` ### read过程分析 流程为: * APP读 * 使用行规程来读 * 无数据则休眠 * UART接收到数据,产生中断 * 中断程序从硬件上读入数据 * 发给行规程 * 行规程处理后存入buffer * 行规程唤醒APP * APP被唤醒后,从行规程buffer中读入数据,返回 #### tty_read 文件:`drivers\tty\tty_io.c` ![](media/26_tty_read.png) #### ldisk read 文件:`drivers\tty\n_tty.c` 函数:`n_tty_read` ![image-20210724095007517](media/27_ldisc_read.png) ```c copy_from_read_buf const unsigned char *from = read_buf_addr(ldata, tail); // return &ldata->read_buf[i & (N_TTY_BUF_SIZE - 1)]; retval = copy_to_user(*b, from, n); ``` #### IMX6ULL数据源头: 中断 文件:`drivers\tty\serial\imx.c` 函数:`imx_rxint` ```c imx_rxint // 读取硬件状态 // 得到数据 // 在对应的uart_port中更新统计信息, 比如sport->port.icount.rx++; // 把数据存入tty_port里的tty_buffer tty_insert_flip_char(port, rx, flg) // 通知行规程来处理 tty_flip_buffer_push(port); tty_schedule_flip(port); queue_work(system_unbound_wq, &buf->work); // 使用工作队列来处理 // 对应flush_to_ldisc函数 ``` ## 驱动write过程分析 ### write过程分析 流程为: * APP写 * 使用行规程来写 * 数据最终存入uart_state->xmit的buffer里 * 硬件发送:怎么发送数据? * 使用硬件驱动中uart_ops->start_tx开始发送 * 具体的发送方法有2种:通过DMA,或通过中断 * 中断方式 * 方法1:直接使能 tx empty中断,一开始tx buffer为空,在中断里填入数据 * 方法2:写部分数据到tx fifo,使能中断,剩下的数据再中断里继续发送 #### tty_write 文件:`drivers\tty\tty_io.c` ![image-20210724114102036](media/28_ldisc_write.png) #### ldisk write 文件:`drivers\tty\n_tty.c` 函数:`n_tty_write` ![image-20210724115304725](media/29_ldisc_write.png) #### uart_write 文件:`drivers\tty\serial\serial_core.c` 函数:`uart_write` ![image-20210724120514017](media/30_uart_write.png) #### 硬件相关的发送IMX6ULL 文件:`drivers\tty\serial\imx.c` 函数:`imx_start_tx`和`imx_txint` ![image-20210724121404872](media/31_imx6ull_start_tx.png) 一开始时,发送buffer肯定为空,会立刻产生中断: ![image-20210724121823272](media/32_imx6ull_txint_isr.png)