UART 子系统
UART物理特性
参数
波特率:一般选波特率都会有9600,19200,115200等选项。其实意思就是每秒传输这么多个比特位数(bit)。
起始位:先发出一个逻辑”0”的信号,表示传输数据的开始。
数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输。
校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。
停止位:它是一个字符数据的结束标志

时序
逻辑电平0和1在不同的设备上定义不同
TTL/CMOS逻辑电平
在xV至5V之间,就认为是逻辑1,在0V至yV之间就为逻辑0

RS-232逻辑电平
在-12V至-3V之间,就认为是逻辑1,在+3V至+12V之间就为逻辑0 ,RS-232的电平比TTL/CMOS高,能传输更远的距离,在工业上用得比较多。

内部逻辑图

要发送数据时,CPU控制内存要发送的数据通过FIFO传给UART单位,UART里面的移位器,依次将数据发送出去,在发送完成后产生中断提醒CPU传输完成。
接收数据时,获取接收引脚的电平,逐位放进接收移位器,再放入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,也支持键盘、显示器,还支持更复杂的功能(比如伪终端)

/dev/tty表示本程序的终端,可能是虚拟终端,也可能是真实的终端。
程序A在前台、后台间切换,它自己的/dev/tty都不会变。例如上图中的shell0程序的终端是
/dev/ttyS0实际shell0访问/dev/tty和访问/dev/ttyS0效果一样/dev/tty0表示前台程序的虚拟终端
你正在操作的界面,就是前台程序
其他后台程序访问
/dev/tty0的话,就是访问前台程序的终端,切换前台程序时,/dev/tty0是变化的// 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驱动框架体系
行规程

如上图所示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驱动程序框架

驱动注册流程分析
驱动和设备树匹配过程
如下图所示:驱动加载或者设备树转换过程中相互匹配的过程

上图的关注点
在驱动的init函数中会调用
uart_register_driver函数,注册一个struct uart_driver。
注册过程如下:
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如果设备树和驱动的compatible属性匹配调用probe函数,在probe函数中会生成一个
struct uart_port结构体,且里面有个ops指针指向imx_pops,imx_pops里面存放着硬件操作串口底层寄存器的函数。
然后调用
uart_add_one_port的函数,把struct uart_port注册这个过程下面会分析。
分析
uart_add_one_port的函数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函数
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
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这一层了
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
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
void __init n_tty_init(void)
{
tty_register_ldisc(N_TTY, &n_tty_ops);
}
以后可以通过标号N_TTY找到这个行规程。
open设备时确定行规程
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

ldisk read
文件:drivers\tty\n_tty.c
函数:n_tty_read
image-20210724095007517
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
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
ldisk write
文件:drivers\tty\n_tty.c
函数:n_tty_write
image-20210724115304725
uart_write
文件:drivers\tty\serial\serial_core.c
函数:uart_write
image-20210724120514017
硬件相关的发送IMX6ULL
文件:drivers\tty\serial\imx.c
函数:imx_start_tx和imx_txint
image-20210724121404872
一开始时,发送buffer肯定为空,会立刻产生中断:
image-20210724121823272