UART 子系统

UART物理特性

参数

  • 波特率:一般选波特率都会有9600,19200,115200等选项。其实意思就是每秒传输这么多个比特位数(bit)。

  • 起始位:先发出一个逻辑”0”的信号,表示传输数据的开始。

  • 数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输。

  • 校验位:数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。

  • 停止位:它是一个字符数据的结束标志

../../_images/image-20221207142543760.png

时序

逻辑电平0和1在不同的设备上定义不同

  • TTL/CMOS逻辑电平

    在xV至5V之间,就认为是逻辑1,在0V至yV之间就为逻辑0

    ../../_images/image-20221207142700248.png

  • RS-232逻辑电平

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

    ../../_images/image-20221207142746200.png

内部逻辑图

../../_images/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,也支持键盘、显示器,还支持更复杂的功能(比如伪终端)

../../_images/image-20221207144455574.png

  • /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驱动框架体系

行规程

../../_images/image-20221207145526675.png

如上图所示Line Discipline就是行规程,为什么会有行规程?

大多数用户都会在输入时犯错,所以退格键会很有用。这当然可以由应用程序本身来实现,但是根据UNIX设计哲学,应用程序应尽可能保持简单。为了方便起见,操作系统提供了一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),这些命令在行规范内默认启用。高级应用程序可以通过将行规范设置为原始模式(raw mode)而不是默认的成熟或准则模式(cooked and canonical)来禁用这些功能。

大多数交互程序(编辑器,邮件客户端,shell,及所有依赖curses或readline的程序)均以原始模式运行,并自行处理所有的行编辑命令。行规范还包含字符回显和回车换行(译者注:\r\n 和 \n)间自动转换的选项。如果你喜欢,可以把它看作是一个原始的内核级sed(1)。

另外,内核提供了几种不同的行规范。一次只能将其中一个连接到给定的串行设备。行规范的默认规则称为N_TTYdrivers/char/n_tty.c)。其他的规则被用于其他目的,例如管理数据包交换(ppp,IrDA,串行鼠标),但这不在本文的讨论范围之内。

TTY驱动程序框架

../../_images/image-20221207145755039.png

驱动注册流程分析

  1. 驱动和设备树匹配过程

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

    ../../_images/image-20221208153910555.png

    上图的关注点

    1. 在驱动的init函数中会调用uart_register_driver函数,注册一个struct uart_driver

      ../../_images/image-20221208155422258.png

      注册过程如下:

      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);
      			}
      		}
      	}
      
      
      
      }
      

      整个过程形成的结果如下:

      ../../_images/image-20221209095419864.pngimage-20221209095419864

    2. 如果设备树和驱动的compatible属性匹配调用probe函数,在probe函数中会生成一个struct uart_port结构体,且里面有个ops指针指向imx_pops,imx_pops里面存放着硬件操作串口底层寄存器的函数。

      ../../_images/image-20221209095953036.png

    3. 然后调用uart_add_one_port的函数,把struct uart_port注册

      这个过程下面会分析。

  2. 分析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

../../_images/26_tty_read.png

ldisk read

文件:drivers\tty\n_tty.c

函数:n_tty_read

../../_images/27_ldisc_read.pngimage-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

../../_images/28_ldisc_write.pngimage-20210724114102036

ldisk write

文件:drivers\tty\n_tty.c

函数:n_tty_write

../../_images/29_ldisc_write.pngimage-20210724115304725

uart_write

文件:drivers\tty\serial\serial_core.c

函数:uart_write

../../_images/30_uart_write.pngimage-20210724120514017

硬件相关的发送IMX6ULL

文件:drivers\tty\serial\imx.c

函数:imx_start_tximx_txint

../../_images/31_imx6ull_start_tx.pngimage-20210724121404872

一开始时,发送buffer肯定为空,会立刻产生中断:

../../_images/32_imx6ull_txint_isr.pngimage-20210724121823272